Compare commits

...

46 Commits

Author SHA1 Message Date
Zhe Fang
32ba453264 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 16:30:16 -05:00
Zhe Fang
d4902329bb chores: update readme 2025-12-31 16:30:14 -05:00
Zhe Fang
83aee8948b Update README.CN.md 2025-12-31 16:13:48 -05:00
Zhe Fang
1f9fab3228 Update README.CN.md 2025-12-31 16:10:42 -05:00
Zhe Fang
7a3a659dfc Update README.CN.md 2025-12-31 16:09:17 -05:00
Zhe Fang
a14afd3eb5 Update README.md 2025-12-31 16:01:37 -05:00
Zhe Fang
c2af7f3186 Update README.CN.md 2025-12-31 16:00:16 -05:00
Zhe Fang
cd026dd2bf Update README.CN.md 2025-12-31 15:56:08 -05:00
Zhe Fang
4bc1a9975d Update README.md 2025-12-31 15:52:05 -05:00
Zhe Fang
07eecf0930 Update README.md 2025-12-31 15:49:22 -05:00
Zhe Fang
35fba5abb0 chores 2025-12-31 15:44:58 -05:00
Zhe Fang
03ef231a3f chores 2025-12-31 15:39:44 -05:00
Zhe Fang
f41879f4e5 chores: add pic 2025-12-31 15:38:18 -05:00
Zhe Fang
bda7510ed6 chores: update readme 2025-12-31 14:28:54 -05:00
Zhe Fang
5ec8c7c61f chores: bump to 1.2.236 2025-12-31 13:32:45 -05:00
Zhe Fang
7e6bd9dade fix: _scrobbleStopwatch 2025-12-31 10:55:56 -05:00
Zhe Fang
56244cb793 fix: scrollable timer 2025-12-31 10:47:43 -05:00
Zhe Fang
cb5f70ab55 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 10:12:29 -05:00
Zhe Fang
3cc018bb1f chores: bump to 1.2.234.0 2025-12-31 10:12:28 -05:00
Zhe Fang
c517d2b008 Update README.md 2025-12-31 10:08:58 -05:00
Zhe Fang
e79f2a0223 Update README.CN.md 2025-12-31 10:07:53 -05:00
Zhe Fang
39122b9147 Update README.CN.md 2025-12-31 10:07:01 -05:00
Zhe Fang
accbdc1806 Update README.md 2025-12-31 10:06:01 -05:00
Zhe Fang
de014d1ad7 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 09:52:12 -05:00
Zhe Fang
cc2ce5f8cf chores: i18n 2025-12-31 09:52:10 -05:00
Zhe Fang
2a2d80436e Update README.CN.md 2025-12-31 09:34:29 -05:00
Zhe Fang
ce3f79f35c Update README.md 2025-12-31 09:33:01 -05:00
Zhe Fang
12e6000cb3 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:43:44 -05:00
Zhe Fang
c1dc684411 fix: scrobble timer is still running when music is paused 2025-12-31 08:43:43 -05:00
Zhe Fang
69ea2cb495 Update README.md 2025-12-31 08:21:33 -05:00
Zhe Fang
e2ee03c4be Update README.CN.md 2025-12-31 08:21:03 -05:00
Zhe Fang
c6fe33d6ae Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:12:12 -05:00
Zhe Fang
7744e145fa chores: i18n 2025-12-31 08:12:10 -05:00
Zhe Fang
0284b1de81 Update README.CN.md 2025-12-30 21:46:32 -05:00
Zhe Fang
108c2cd34b Update README.md 2025-12-30 21:45:56 -05:00
Zhe Fang
390e30f7f5 chores: bump to v1.2.232.0 2025-12-30 20:54:40 -05:00
Zhe Fang
900774668d chores: i18n 2025-12-30 20:09:02 -05:00
Zhe Fang
6ca2d1f897 chores 2025-12-30 19:49:54 -05:00
Zhe Fang
164bd077b8 fix: app crash when audio device was not found 2025-12-30 14:30:10 -05:00
Zhe Fang
8ec71fcfb7 chores: update StatsDashboardControl 2025-12-30 14:04:08 -05:00
Zhe Fang
f39ad54df8 fix: record play history 2025-12-30 13:02:23 -05:00
Zhe Fang
9b809983df chores: i18n 2025-12-30 08:59:30 -05:00
Zhe Fang
8006b3a443 chores: i18n 2025-12-30 08:41:12 -05:00
Zhe Fang
26a7454de2 feat: play history 2025-12-30 08:02:48 -05:00
Zhe Fang
0793a074cf fix: add to playlists 2025-12-29 16:11:21 -05:00
Zhe Fang
125bf1682e chores 2025-12-29 12:15:39 -05:00
87 changed files with 3447 additions and 1279 deletions

View File

@@ -12,7 +12,7 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.1.221.0" />
Version="1.2.236.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

View File

@@ -76,6 +76,7 @@
<converter:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" />
<converter:FileSourceTypeToIconConverter x:Key="FileSourceTypeToIconConverter" />
<converter:PathToImageConverter x:Key="PathToImageConverter" />
<converter:DoubleToDecimalConverter x:Key="DoubleToDecimalConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />

View File

@@ -1,8 +1,6 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.FileSystemService;
@@ -10,42 +8,49 @@ using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslationService;
using BetterLyrics.WinUI3.Services.TransliterationService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching; // 关键:用于线程调度
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.Resources;
using Microsoft.Windows.Globalization;
using Microsoft.Windows.AppLifecycle; // 关键App生命周期管理
using Serilog;
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3
{
public partial class App : Application
{
private Window? m_window;
private readonly ILogger<App> _logger;
public static new App Current => (App)Application.Current;
private static Mutex? _instanceMutex;
private readonly string _appKey = Windows.ApplicationModel.Package.Current.Id.FamilyName;
public App()
{
this.InitializeComponent();
// Must be done before InitializeComponent
if (!TryHandleSingleInstance())
{
// 如果移交成功直接退出当前进程
Environment.Exit(0);
return;
}
EnsureSingleInstance();
this.InitializeComponent();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
PathHelper.EnsureDirectories();
@@ -53,32 +58,94 @@ namespace BetterLyrics.WinUI3
_logger = Ioc.Default.GetRequiredService<ILogger<App>>();
// 注册全局异常捕获
UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
private void EnsureSingleInstance()
/// <summary>
/// 处理单实例逻辑。
/// 返回 true 表示我是主实例,继续运行。
/// 返回 false 表示我是第二个实例,已通知主实例,我应该退出。
/// </summary>
private bool TryHandleSingleInstance()
{
_instanceMutex = new Mutex(true, Constants.App.AppName, out bool createdNew);
// 尝试查找或注册当前实例
var mainInstance = AppInstance.FindOrRegisterForKey(_appKey);
if (!createdNew)
// 如果当前实例就是注册的那个主实例
if (mainInstance.IsCurrent)
{
User32.MessageBox(HWND.NULL, new ResourceLoader().GetString("TryRunMultipleInstance"), null, User32.MB_FLAGS.MB_APPLMODAL);
Environment.Exit(0);
// 监听 "Activated" 事件。
// 当第二个实例启动并重定向过来时,这个事件会被触发。
mainInstance.Activated += OnMainInstanceActivated;
return true;
}
else
{
// 我不是主实例,我是后来者。
// 获取当前实例的激活参数(比如是通过文件双击打开的,这里能拿到文件路径)
var args = AppInstance.GetCurrent().GetActivatedEventArgs();
// 将激活请求重定向给主实例
// 注意:这里是同步等待,确保发送成功后再退出
try
{
mainInstance.RedirectActivationToAsync(args).AsTask().Wait();
}
catch (Exception)
{
// 即使重定向失败,作为第二个实例也应该退出
}
return false;
}
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
/// <summary>
/// 当第二个实例试图启动时,主实例会收到此回调
/// </summary>
private void OnMainInstanceActivated(object? sender, AppActivationArguments e)
{
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
// 这个事件是在后台线程触发的,必须切回 UI 线程操作窗口
m_window?.DispatcherQueue.TryEnqueue(() =>
{
HandleActivation();
});
}
/// <summary>
/// 唤醒逻辑
/// </summary>
private void HandleActivation()
{
WindowHook.OpenOrShowWindow<LyricsWindowSwitchWindow>();
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
// 初始化数据库
await EnsureDatabasesAsync();
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
var fileSystemService = Ioc.Default.GetRequiredService<IFileSystemService>();
// 开始后台扫描任务
foreach (var item in settingsService.AppSettings.LocalMediaFolders)
{
if (item.LastSyncTime == null)
{
_ = Task.Run(async () => await fileSystemService.ScanMediaFolderAsync(item, CancellationToken.None));
}
}
fileSystemService.StartAllFolderTimers();
WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// 初始化托盘
m_window = WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// 根据设置打开歌词窗口
if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow)
{
var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault);
@@ -94,12 +161,101 @@ namespace BetterLyrics.WinUI3
}
}
}
// 根据设置自动打开主界面
if (settingsService.AppSettings.MusicGallerySettings.AutoOpen)
{
WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
}
}
private async Task EnsureDatabasesAsync()
{
var playHistoryFactory = Ioc.Default.GetRequiredService<IDbContextFactory<PlayHistoryDbContext>>();
var fileCacheFactory = Ioc.Default.GetRequiredService<IDbContextFactory<FilesIndexDbContext>>();
await SafeInitDatabaseAsync(
"PlayHistory",
PathHelper.PlayHistoryPath,
async () =>
{
using var db = await playHistoryFactory.CreateDbContextAsync();
await db.Database.EnsureCreatedAsync();
},
isCritical: true
);
await SafeInitDatabaseAsync(
"FileCache",
PathHelper.FilesIndexPath,
async () =>
{
using var db = await fileCacheFactory.CreateDbContextAsync();
await db.Database.EnsureCreatedAsync();
},
isCritical: false
);
}
private async Task SafeInitDatabaseAsync(string dbName, string dbPath, Func<Task> initAction, bool isCritical)
{
try
{
await initAction();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DB Error] {dbName} init failed: {ex.Message}");
try
{
if (File.Exists(dbPath))
{
// 尝试清理连接池
SqliteConnection.ClearAllPools();
if (isCritical)
{
var backupPath = dbPath + ".bak_" + DateTime.Now.ToString("yyyyMMddHHmmss");
File.Move(dbPath, backupPath, true);
await ShowErrorDialogAsync("Database Recovery", $"Database {dbName} is damaged, the old database has been backed up to {backupPath}, and the program will create a new database.");
}
else
{
File.Delete(dbPath);
}
}
await initAction();
System.Diagnostics.Debug.WriteLine($"[DB Info] {dbName} recovered successfully.");
}
catch (Exception fatalEx)
{
System.Diagnostics.Debug.WriteLine($"[] : {fatalEx.Message}");
await ShowErrorDialogAsync("Fatal Error", $"{dbName} recovery failed, please delete the file at {dbPath} and try again by restarting the program. ({fatalEx.Message})");
}
}
}
private async Task ShowErrorDialogAsync(string title, string content)
{
// 这里假设 m_window 已经存在。如果没有显示主窗口,这个弹窗可能无法显示。
// 在 App 启动极早期的错误,可能需要退化为 Log 或者 System.Diagnostics.Process.Start 打开记事本报错
if (m_window != null)
{
m_window.DispatcherQueue.TryEnqueue(async () =>
{
var dialog = new Microsoft.UI.Xaml.Controls.ContentDialog
{
Title = title,
Content = content,
CloseButtonText = "OK",
XamlRoot = m_window.Content?.XamlRoot // 确保 Content 不为空
};
if (dialog.XamlRoot != null) await dialog.ShowAsync();
});
}
}
private static void ConfigureServices()
{
Log.Logger = new LoggerConfiguration()
@@ -107,14 +263,19 @@ namespace BetterLyrics.WinUI3
.WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
// Register services
Ioc.Default.ConfigureServices(
new ServiceCollection()
// 数据库工厂
.AddDbContextFactory<PlayHistoryDbContext>(options => options.UseSqlite($"Data Source={PathHelper.PlayHistoryPath}"))
.AddDbContextFactory<FilesIndexDbContext>(options => options.UseSqlite($"Data Source={PathHelper.FilesIndexPath}"))
// 日志
.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddSerilog();
})
// Services
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IMediaSessionsService, MediaSessionsService>()
@@ -126,6 +287,8 @@ namespace BetterLyrics.WinUI3
.AddSingleton<IDiscordService, DiscordService>()
.AddSingleton<ILocalizationService, LocalizationService>()
.AddSingleton<IFileSystemService, FileSystemService>()
.AddSingleton<IPlayHistoryService, PlayHistoryService>()
// ViewModels
.AddSingleton<AppSettingsControlViewModel>()
.AddSingleton<PlaybackSettingsControlViewModel>()
@@ -140,6 +303,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<MusicGalleryPageViewModel>()
.AddSingleton<AboutControlViewModel>()
.AddSingleton<MusicGalleryWindowViewModel>()
.AddSingleton<StatsDashboardControlViewModel>()
.AddTransient<NowPlayingWindowViewModel>()
.AddTransient<NowPlayingPageViewModel>()
@@ -157,7 +321,8 @@ namespace BetterLyrics.WinUI3
private void CurrentDomain_FirstChanceException(object? sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
{
_logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
// FirstChance 异常非常多(比如内部 try-catch 也会触发),通常建议只在 Debug 模式记录,或者过滤特定类型
// _logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
}
private void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
@@ -170,4 +335,4 @@ namespace BetterLyrics.WinUI3
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -46,6 +46,7 @@
<None Remove="Controls\PropertyRow.xaml" />
<None Remove="Controls\RemoteServerConfigControl.xaml" />
<None Remove="Controls\ShortcutTextBox.xaml" />
<None Remove="Controls\StatsDashboardControl.xaml" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Controls\WindowSettingsControl.xaml" />
<None Remove="Views\LyricsSearchWindow.xaml" />
@@ -69,6 +70,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.251219" />
@@ -85,6 +87,10 @@
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
<PackageReference Include="Interop.UIAutomationClient" Version="10.19041.0" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
@@ -95,7 +101,6 @@
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SMBLibrary" Version="1.5.5.1" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Drawing.Common" Version="10.0.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.1" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
@@ -128,6 +133,10 @@
<ItemGroup>
<TrimmerRootAssembly Include="FlaUI.UIA3" />
<TrimmerRootAssembly Include="Interop.UIAutomationClient" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Abstractions" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Relational" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Sqlite" />
<TrimmerRootAssembly Include="NAudio.Wasapi" />
<TrimmerRootAssembly Include="TagLibSharp" />
<TrimmerRootAssembly Include="Vanara.PInvoke.DwmApi" />
@@ -215,6 +224,9 @@
<Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\OriginalSoundHQPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -246,6 +258,11 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\StatsDashboardControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\FontFamilyAutoSuggestBox.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -1,6 +1,6 @@
namespace BetterLyrics.WinUI3.Constants
{
public static class PlayerID
public static class PlayerId
{
public const string LXMusic = "cn.toside.music.desktop";
public const string LXMusicPortable = "lx-music-desktop.exe";
@@ -25,5 +25,6 @@
public const string MoeKoeMusic = "cn.MoeKoe.Music";
public const string MoeKoeMusicAlternative = "electron.app.MoeKoe Music";
public const string Listen1 = "com.listen1.listen1";
public const string OriginalSoundHQPlayer = "SennpaiStudio.528762A6196EF_z79ft30j24epr!App";
}
}

View File

@@ -24,5 +24,6 @@
public const string SaltPlayerForWindowsSteam = "Salt Player for Windows (Steam)";
public const string MoeKoeMusic = "MoeKoe Music";
public const string Listen1 = "Listen 1";
public const string OriginalSoundHQPlayer = "Original Sound HQ Player";
}
}

View File

@@ -118,13 +118,21 @@
</HyperlinkButton>
<HyperlinkButton Content="爱发电" NavigateUri="{x:Bind const:Link.Afdian}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="*" />
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="*" />
<TextBlock
x:Uid="SetingsPageThanks"
Grid.Column="1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</StackPanel>
</dev:SettingsCard>
@@ -182,6 +190,12 @@
</dev:SettingsExpander.ItemsHeader>
</dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageSettingsPlayHistory" Visibility="Collapsed">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button x:Uid="SettingsPageExportPlayHistoryButton" Command="{x:Bind ViewModel.ExportPlayHistoryCommand}" />
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageFixedTimeStep" Visibility="Collapsed">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.AdvancedSettings.IsFixedTimeStep, Mode=TwoWay}" />
</dev:SettingsCard>

View File

@@ -42,7 +42,6 @@ namespace BetterLyrics.WinUI3.Controls
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly ILastFMService _lastFMService = Ioc.Default.GetRequiredService<ILastFMService>();
private readonly LyricsRenderer _lyricsRenderer = new();
private readonly FluidBackgroundRenderer _fluidRenderer = new();
@@ -660,22 +659,6 @@ namespace BetterLyrics.WinUI3.Controls
_songPosition += elapsedTime;
_totalPlayedTime += elapsedTime;
_songPositionWithOffset = _songPosition + TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
CheckAndScrobbleLastFM();
}
}
private void CheckAndScrobbleLastFM()
{
bool isEnabled = _mediaSessionsService.CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (!isEnabled || _isLastFMTracked) return;
var songInfo = _mediaSessionsService.CurrentSongInfo;
if (songInfo == null || songInfo.Duration <= 0) return;
if (_totalPlayedTime.TotalSeconds >= songInfo.Duration * 0.5)
{
_isLastFMTracked = true;
_lastFMService.TrackAsync(songInfo);
}
}

View File

@@ -5,6 +5,7 @@
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dev="using:DevWinUI"
xmlns:enums="using:BetterLyrics.WinUI3.Enums"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -88,14 +89,18 @@
</dev:SettingsCard>
</dev:SettingsExpander.Items>
<dev:SettingsExpander.ItemsFooter>
<dev:SettingsExpander.ItemsHeader>
<StackPanel>
<!-- Index info -->
<InfoBar
IsClosable="False"
IsOpen="{x:Bind IsIndexing, Mode=OneWay}"
Message="{x:Bind IndexingStatusText, Mode=OneWay}" />
<ProgressBar Visibility="{x:Bind IsIndexing, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" Value="{x:Bind IndexingProgress, Mode=OneWay}">
IsOpen="True"
Message="{x:Bind StatusText, Mode=OneWay}"
Severity="{x:Bind StatusSeverity, Mode=OneWay}" />
<ProgressBar
Background="Transparent"
Visibility="{x:Bind IsProcessing, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
Value="{x:Bind IndexingProgress, Mode=OneWay}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind IndexingProgress, Mode=OneWay}"
@@ -111,14 +116,8 @@
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</ProgressBar>
<!-- Clean up info -->
<InfoBar
IsClosable="False"
IsOpen="{x:Bind IsCleaningUp, Mode=OneWay}"
Message="{x:Bind CleaningUpStatusText, Mode=OneWay}" />
<ProgressBar IsIndeterminate="True" Visibility="{x:Bind IsCleaningUp, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</StackPanel>
</dev:SettingsExpander.ItemsFooter>
</dev:SettingsExpander.ItemsHeader>
</dev:SettingsExpander>
</DataTemplate>

View File

@@ -11,7 +11,6 @@
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<Grid
x:Name="BottomCommandGrid"
Background="{ThemeResource LayerOnMicaBaseAltFillColorDefaultBrush}"
@@ -113,7 +112,58 @@
x:Name="BottomCenterCommandStackPanel"
Padding="16"
Orientation="Horizontal"
Spacing="3">
Spacing="12">
<!-- Playback order -->
<Button
Grid.Column="2"
Click="PlaybackOrderButton_Click"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowPlaybackOrderButton, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ToolTipService.ToolTip>
<ToolTip>
<Grid>
<TextBlock x:Name="PlaybackRepeatAllHint" x:Uid="MusicGalleryPageQueueLoop" />
<TextBlock x:Name="PlaybackRepeatOneHint" x:Uid="MusicGalleryPageSingleLoop" />
<TextBlock x:Name="PlaybackShuffleHint" x:Uid="MusicGalleryPageQueueRandom" />
</Grid>
</ToolTip>
</ToolTipService.ToolTip>
<Button.Content>
<Grid>
<!-- Repeat all -->
<FontIcon
x:Name="PlaybackRepeatAll"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8EE;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Repeat one -->
<FontIcon
x:Name="PlaybackRepeatOne"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8ED;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Shuffle -->
<FontIcon
x:Name="PlaybackShuffle"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8B1;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
</Button.Content>
</Button>
<!-- 上一曲目 -->
<Button
Command="{x:Bind ViewModel.PreviousSongCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -164,6 +214,13 @@
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE623;}"
Style="{StaticResource GhostButtonStyle}" />
<!-- 播放队列按钮 -->
<ToggleButton
Click="PlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8FD;}"
Style="{StaticResource GhostToggleButtonStyle}"
Visibility="{x:Bind ShowPlayingQueueButton, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
</Grid>
@@ -427,7 +484,58 @@
</Grid.ContextFlyout>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="PlaybackOrderState">
<VisualState x:Name="RepeatAll">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="1" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="0" />
<Setter Target="PlaybackShuffle.Opacity" Value="0" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Visible" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="RepeatOne">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="1" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="0" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="1" />
<Setter Target="PlaybackShuffle.Opacity" Value="0" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Visible" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Shuffle">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="2" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="0" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="0" />
<Setter Target="PlaybackShuffle.Opacity" Value="1" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -1,5 +1,7 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
@@ -12,6 +14,7 @@ using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Numerics;
using BetterLyrics.WinUI3.Extensions;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -46,6 +49,42 @@ public sealed partial class NowPlayingBar : UserControl,
public static readonly DependencyProperty ShowSongInfoProperty =
DependencyProperty.Register(nameof(ShowSongInfo), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowPlayingQueueButton
{
get { return (bool)GetValue(ShowPlayingQueueButtonProperty); }
set { SetValue(ShowPlayingQueueButtonProperty, value); }
}
public static readonly DependencyProperty ShowPlayingQueueButtonProperty =
DependencyProperty.Register(nameof(ShowPlayingQueueButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowPlaybackOrderButton
{
get { return (bool)GetValue(ShowPlaybackOrderButtonProperty); }
set { SetValue(ShowPlaybackOrderButtonProperty, value); }
}
public static readonly DependencyProperty ShowPlaybackOrderButtonProperty =
DependencyProperty.Register(nameof(ShowPlaybackOrderButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public PlaybackOrder PlaybackOrder
{
get { return (PlaybackOrder)GetValue(PlaybackOrderProperty); }
set { SetValue(PlaybackOrderProperty, value); }
}
public static readonly DependencyProperty PlaybackOrderProperty =
DependencyProperty.Register(nameof(PlaybackOrder), typeof(PlaybackOrder), typeof(NowPlayingBar), new PropertyMetadata(PlaybackOrder.RepeatAll));
public bool IsPlayingQueueOpened
{
get { return (bool)GetValue(IsPlayingQueueOpenedProperty); }
set { SetValue(IsPlayingQueueOpenedProperty, value); }
}
public static readonly DependencyProperty IsPlayingQueueOpenedProperty =
DependencyProperty.Register(nameof(IsPlayingQueueOpened), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool IsCompactMode
{
get { return (bool)GetValue(IsCompactModeProperty); }
@@ -210,12 +249,12 @@ public sealed partial class NowPlayingBar : UserControl,
private void SongInfoStackPanel_Tapped(object sender, TappedRoutedEventArgs e)
{
SongInfoTapped?.Invoke(this, EventArgs.Empty);
SongInfoTapped?.Invoke(sender, EventArgs.Empty);
}
private void TimeStackPanel_Tapped(object sender, TappedRoutedEventArgs e)
{
TimeTapped?.Invoke(this, EventArgs.Empty);
TimeTapped?.Invoke(sender, EventArgs.Empty);
}
private void BottomCommandGrid_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -262,6 +301,16 @@ public sealed partial class NowPlayingBar : UserControl,
}
}
private void PlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
IsPlayingQueueOpened = !IsPlayingQueueOpened;
}
private void PlaybackOrderButton_Click(object sender, RoutedEventArgs e)
{
PlaybackOrder = PlaybackOrder.GetNext();
}
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)

View File

@@ -12,13 +12,13 @@ namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class RemoteServerConfigControl : UserControl
{
private readonly string _protocolType;
private readonly FileSourceType _fileSourceType;
private readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
public RemoteServerConfigControl(string protocolType)
public RemoteServerConfigControl(FileSourceType fileSourceType)
{
this.InitializeComponent();
_protocolType = protocolType;
_fileSourceType = fileSourceType;
SetupDefaults();
CheckPathForWarning();
@@ -26,7 +26,7 @@ namespace BetterLyrics.WinUI3.Controls
private void SetupDefaults()
{
if (_protocolType.Equals("Local", StringComparison.OrdinalIgnoreCase))
if (_fileSourceType == FileSourceType.Local)
{
RemoteFieldsPanel.Visibility = Visibility.Collapsed;
AuthFieldsPanel.Visibility = Visibility.Collapsed;
@@ -41,17 +41,17 @@ namespace BetterLyrics.WinUI3.Controls
RemoteFieldsPanel.Visibility = Visibility.Visible;
AuthFieldsPanel.Visibility = Visibility.Visible;
switch (_protocolType.ToUpper())
switch (_fileSourceType)
{
case "SMB":
case FileSourceType.SMB:
PortBox.Value = 445;
PathBox.PlaceholderText = "SharedMusic";
break;
case "FTP":
case FileSourceType.FTP:
PortBox.Value = 21;
PathBox.PlaceholderText = "/pub/music";
break;
case "WEBDAV":
case FileSourceType.WebDAV:
PortBox.Value = 80;
PathBox.PlaceholderText = "/dav/music";
break;
@@ -62,15 +62,15 @@ namespace BetterLyrics.WinUI3.Controls
private string GetScheme()
{
string scheme = string.Empty;
switch (_protocolType.ToUpper())
switch (_fileSourceType)
{
case "SMB":
case FileSourceType.SMB:
scheme = "smb";
break;
case "FTP":
case FileSourceType.FTP:
scheme = "ftp";
break;
case "WEBDAV":
case FileSourceType.WebDAV:
scheme = "https";
break;
}
@@ -81,7 +81,7 @@ namespace BetterLyrics.WinUI3.Controls
{
string finalName = HostBox.Text.Trim();
if (_protocolType.Equals("Local", StringComparison.OrdinalIgnoreCase))
if (_fileSourceType == FileSourceType.Local)
{
if (string.IsNullOrWhiteSpace(PathBox.Text))
throw new ArgumentException(_localizationService.GetLocalizedString("RemoteServerConfigControlPathRequired"));
@@ -109,17 +109,15 @@ namespace BetterLyrics.WinUI3.Controls
}
else
{
finalName = $"{_protocolType} - {HostBox.Text}";
finalName = $"{_fileSourceType} - {HostBox.Text}";
}
Enum.TryParse(_protocolType, true, out FileSourceType sourceType);
string scheme = GetScheme();
var folder = new MediaFolder
{
Name = finalName,
SourceType = sourceType,
SourceType = _fileSourceType,
UriScheme = scheme,
UriHost = HostBox.Text.Trim(), // ȥ<><C8A5><EFBFBD><EFBFBD>β<EFBFBD>ո<EFBFBD>
@@ -144,12 +142,7 @@ namespace BetterLyrics.WinUI3.Controls
{
ProgressBar.Visibility = visibility;
}
private void PathBox_TextChanged(object sender, TextChangedEventArgs e)
{
CheckPathForWarning();
}
private void CheckPathForWarning()
{
string? path = PathBox.Text?.Trim();
@@ -178,6 +171,11 @@ namespace BetterLyrics.WinUI3.Controls
}
}
private void PathBox_TextChanged(object sender, TextChangedEventArgs e)
{
CheckPathForWarning();
}
private async void BrowseButton_Click(object sender, RoutedEventArgs e)
{
try

View File

@@ -0,0 +1,302 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.StatsDashboardControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:BetterLyrics.WinUI3.Converter"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:statsmodels="using:BetterLyrics.WinUI3.Models.Stats"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<UserControl.Resources>
<Style x:Key="StatsCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource LayerFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="16" />
<Setter Property="Margin" Value="0,0,12,12" />
</Style>
</UserControl.Resources>
<Grid Margin="0,24,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="20,0">
<Pivot x:Name="TimeRangePivot" SelectionChanged="Pivot_SelectionChanged">
<PivotItem x:Uid="StatsDashboardControlToday" Tag="Day" />
<PivotItem x:Uid="StatsDashboardControlThisWeek" Tag="Week" />
<PivotItem x:Uid="StatsDashboardControlThisMonth" Tag="Month" />
<PivotItem x:Uid="StatsDashboardControlThisQuarter" Tag="Quarter" />
<PivotItem x:Uid="StatsDashboardControlThisYear" Tag="Year" />
</Pivot>
</Grid>
<ScrollViewer Grid.Row="1" Padding="20,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,20,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 总播放时长 -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE916;" />
<TextBlock x:Uid="StatsDashboardControlTotalDuration" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<StackPanel
Margin="0,8,0,0"
Orientation="Horizontal"
Spacing="4">
<TextBlock
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TotalDuration.TotalHours, Mode=OneWay, Converter={StaticResource DoubleToDecimalConverter}}" />
<TextBlock
Margin="0,0,0,2"
VerticalAlignment="Bottom"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Opacity="0.8"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="Hrs" />
</StackPanel>
</StackPanel>
</Border>
<!-- 总播放歌曲次数 -->
<Border Grid.Column="1" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE8D6;" />
<TextBlock x:Uid="StatsDashboardControlTracksPlayed" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<TextBlock
Margin="0,8,0,0"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TotalTracksPlayed, Mode=OneWay}" />
</StackPanel>
</Border>
<!-- Top source -->
<Border
Grid.Column="2"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
Opacity="0.8"
Orientation="Horizontal"
Spacing="8">
<FontIcon FontSize="14" Glyph="&#xEC4A;" />
<TextBlock x:Uid="StatsDashboardControlTopSource" Style="{ThemeResource CaptionTextBlockStyle}" />
</StackPanel>
<TextBlock
Margin="0,8,0,0"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TopPlayerName, Mode=OneWay}" />
</StackPanel>
</Border>
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1.5*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Top artists -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlTopArtists"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind ViewModel.TopArtists, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="statsmodels:ArtistPlayCount">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Spacing="10">
<PersonPicture
Width="32"
Height="32"
DisplayName="{x:Bind Artist}" />
<TextBlock
VerticalAlignment="Center"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind Artist}" />
</StackPanel>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
<!-- Top sources -->
<Border
Grid.Column="1"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlSources"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind ViewModel.PlayerStats, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:PlayerStatDisplayItem">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
FontSize="13"
Style="{ThemeResource BodyTextBlockStyle}"
Text="{x:Bind PlayerName}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
</Grid>
<!-- Top song -->
<Border
Grid.Row="2"
Margin="0,0,0,20"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlTopSongs"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ListView
Grid.Row="1"
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind ViewModel.TopSongs, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="statsmodels:SongPlayCount">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
Width="40"
Height="40"
Margin="0,0,12,0"
Background="{ThemeResource LayerFillColorAltBrush}"
CornerRadius="4">
<FontIcon
FontSize="16"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Glyph="&#xE8D6;" />
</Grid>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Style="{ThemeResource BodyStrongTextBlockStyle}" Text="{x:Bind Title}" />
<TextBlock
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind Artist}" />
</StackPanel>
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Border>
</Grid>
</ScrollViewer>
<Button
Grid.Row="1"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.GenerateTestDataCommand}"
Content="Generate test data"
Visibility="Collapsed" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using BetterLyrics.WinUI3.Enums;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Controls;
public sealed partial class StatsDashboardControl : UserControl
{
public StatsDashboardControlViewModel ViewModel => (StatsDashboardControlViewModel)DataContext;
public StatsDashboardControl()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<StatsDashboardControlViewModel>();
this.Loaded += StatsDashboardControl_Loaded;
}
private async void StatsDashboardControl_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
await ViewModel.LoadDataAsync(StatsRange.Day);
}
private async void Pivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ViewModel == null) return;
if (TimeRangePivot.SelectedItem is PivotItem item && item.Tag is string tag)
{
var range = tag switch
{
"Day" => StatsRange.Day,
"Week" => StatsRange.Week,
"Month" => StatsRange.Month,
"Quarter" => StatsRange.Quarter,
"Year" => StatsRange.Year,
_ => StatsRange.Day
};
await ViewModel.LoadDataAsync(range);
}
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Converter
{
public partial class DoubleToDecimalConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value == null) return string.Empty;
if (double.TryParse(value.ToString(), out double number))
{
int decimalPlaces = 2;
if (parameter != null && int.TryParse(parameter.ToString(), out int parsedParams))
{
decimalPlaces = parsedParams;
}
return number.ToString($"F{decimalPlaces}");
}
return value.ToString() ?? "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -15,7 +15,7 @@ namespace BetterLyrics.WinUI3.Converter
FileSourceType.Local => "\uE8B7", // Folder
FileSourceType.SMB => "\uE839", // Network
FileSourceType.FTP => "\uE838", // Globe
FileSourceType.WebDav => "\uE753", // Cloud
FileSourceType.WebDAV => "\uE753", // Cloud
_ => "\uE8B7"
};
}

View File

@@ -3,18 +3,37 @@ using System;
namespace BetterLyrics.WinUI3.Converter
{
public class MillisecondsToFormattedTimeConverter : IValueConverter
public partial class MillisecondsToFormattedTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is int milliseconds)
double? milliseconds = null;
if (value is int iVal) milliseconds = iVal;
else if (value is double dVal) milliseconds = dVal;
else if (value is long lVal) milliseconds = lVal;
if (milliseconds.HasValue)
{
return TimeSpan.FromMilliseconds(milliseconds).ToString(@"mm\:ss\.fff");
}
else if (value is double doubleMilliseconds)
{
return TimeSpan.FromMilliseconds(doubleMilliseconds).ToString(@"mm\:ss\.fff");
var ts = TimeSpan.FromMilliseconds(milliseconds.Value);
string? format = parameter?.ToString();
if (string.IsNullOrEmpty(format))
{
format = @"mm\:ss\.fff";
}
try
{
return ts.ToString(format);
}
catch (FormatException)
{
return ts.ToString();
}
}
return value?.ToString() ?? "";
}

View File

@@ -5,6 +5,6 @@
Local,
SMB,
FTP,
WebDav
WebDAV
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Enums
{
public enum StatsRange
{
Day,
Week,
Month,
Quarter,
Year
}
}

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Models;
using System;
namespace BetterLyrics.WinUI3.Extensions
{
@@ -30,6 +31,22 @@ namespace BetterLyrics.WinUI3.Extensions
songInfo.Album = value;
return songInfo;
}
public PlayHistoryItem? ToPlayHistoryItem(double actualPlayedMs)
{
if (songInfo == null) return null;
return new PlayHistoryItem
{
Title = songInfo.Title,
Artist = songInfo.DisplayArtists,
Album = songInfo.Album,
PlayerId = songInfo.PlayerId ?? "N/A",
TotalDurationMs = songInfo.DurationMs,
DurationPlayedMs = actualPlayedMs,
StartedAt = DateTime.Now.AddMilliseconds(-actualPlayedMs)
};
}
}
}
}

View File

@@ -9,6 +9,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ude;
namespace BetterLyrics.WinUI3.Helper
@@ -29,6 +30,18 @@ namespace BetterLyrics.WinUI3.Helper
return Encoding.GetEncoding(encoding);
}
public static async Task CopyFileAsync(string sourcePath, string destinationPath)
{
var dir = Path.GetDirectoryName(destinationPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var destinationStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await sourceStream.CopyToAsync(destinationStream);
}
}
public static string SanitizeFileName(string fileName, char replacement = '_')
{
var invalidChars = Path.GetInvalidFileNameChars();

View File

@@ -33,6 +33,7 @@ namespace BetterLyrics.WinUI3.Helper
public static string SaltPlayerForWindowsLogoPath => Path.Combine(AssetsFolder, "SaltPlayerForWindows.png");
public static string MoeKoeMusicLogoPath => Path.Combine(AssetsFolder, "MoeKoeMusic.png");
public static string Listen1LogoPath => Path.Combine(AssetsFolder, "Listen1.png");
public static string OriginalSoundHQPlayerLogoPath => Path.Combine(AssetsFolder, "OriginalSoundHQPlayer.png");
public static string UnknownPlayerLogoPath => Path.Combine(AssetsFolder, "Question.png");
public static string LogDirectory => Path.Combine(CacheFolder, "logs");
@@ -56,8 +57,9 @@ namespace BetterLyrics.WinUI3.Helper
public static string iTunesAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "itunes");
public static string LocalAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "local");
public static string PlayQueuePath => Path.Combine(CacheFolder, "play-queue.m3u");
public static string FilesCachePath => Path.Combine(CacheFolder, "files_cache.db");
public static string PlayQueuePath => Path.Combine(LocalFolder, "play-queue.m3u");
public static string PlayHistoryPath => Path.Combine(LocalFolder, "play-history.db");
public static string FilesIndexPath => Path.Combine(LocalFolder, "files-index.db");
public static void EnsureDirectories()
{

View File

@@ -4,7 +4,7 @@ using System.Text.RegularExpressions;
namespace BetterLyrics.WinUI3.Helper
{
public static class PlayerIDHelper
public static class PlayerIdHelper
{
private static readonly List<string> neteaseFamilyRegex =
[
@@ -25,64 +25,66 @@ namespace BetterLyrics.WinUI3.Helper
return false;
}
public static bool IsLXMusic(string? id) => id is PlayerID.LXMusic or PlayerID.LXMusicPortable;
public static bool IsLXMusic(string? id) => id is PlayerId.LXMusic or PlayerId.LXMusicPortable;
public static bool IsAppleMusic(string? id) => id is PlayerID.AppleMusic or PlayerID.AppleMusicAlternative;
public static bool IsAppleMusic(string? id) => id is PlayerId.AppleMusic or PlayerId.AppleMusicAlternative;
public static bool IsBetterLyrics(string? id) => id is PlayerID.BetterLyrics or PlayerID.BetterLyricsDebug;
public static bool IsBetterLyrics(string? id) => id is PlayerId.BetterLyrics or PlayerId.BetterLyricsDebug;
public static string? GetDisplayName(string? id) => id switch
{
PlayerID.Spotify => PlayerName.Spotify,
PlayerID.AppleMusic => PlayerName.AppleMusic,
PlayerID.iTunes => PlayerName.iTunes,
PlayerID.KugouMusic => PlayerName.KugouMusic,
PlayerID.NetEaseCloudMusic => PlayerName.NetEaseCloudMusic,
PlayerID.QQMusic => PlayerName.QQMusic,
PlayerID.LXMusic => PlayerName.LXMusic,
PlayerID.LXMusicPortable => PlayerName.LXMusicPortable,
PlayerID.MediaPlayerWindows11 => PlayerName.MediaPlayerWindows11,
PlayerID.AIMP => PlayerName.AIMP,
PlayerID.Foobar2000 => PlayerName.Foobar2000,
PlayerID.MusicBee => PlayerName.MusicBee,
PlayerID.PotPlayer => PlayerName.PotPlayer,
PlayerID.Chrome => PlayerName.Chrome,
PlayerID.Edge => PlayerName.Edge,
PlayerID.BetterLyrics => PlayerName.BetterLyrics,
PlayerID.BetterLyricsDebug => PlayerName.BetterLyricsDebug,
PlayerID.SaltPlayerForWindowsMS => PlayerName.SaltPlayerForWindowsMS,
PlayerID.SaltPlayerForWindowsSteam => PlayerName.SaltPlayerForWindowsSteam,
PlayerID.MoeKoeMusic => PlayerName.MoeKoeMusic,
PlayerID.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
PlayerID.Listen1 => PlayerName.Listen1,
PlayerId.Spotify => PlayerName.Spotify,
PlayerId.AppleMusic => PlayerName.AppleMusic,
PlayerId.iTunes => PlayerName.iTunes,
PlayerId.KugouMusic => PlayerName.KugouMusic,
PlayerId.NetEaseCloudMusic => PlayerName.NetEaseCloudMusic,
PlayerId.QQMusic => PlayerName.QQMusic,
PlayerId.LXMusic => PlayerName.LXMusic,
PlayerId.LXMusicPortable => PlayerName.LXMusicPortable,
PlayerId.MediaPlayerWindows11 => PlayerName.MediaPlayerWindows11,
PlayerId.AIMP => PlayerName.AIMP,
PlayerId.Foobar2000 => PlayerName.Foobar2000,
PlayerId.MusicBee => PlayerName.MusicBee,
PlayerId.PotPlayer => PlayerName.PotPlayer,
PlayerId.Chrome => PlayerName.Chrome,
PlayerId.Edge => PlayerName.Edge,
PlayerId.BetterLyrics => PlayerName.BetterLyrics,
PlayerId.BetterLyricsDebug => PlayerName.BetterLyricsDebug,
PlayerId.SaltPlayerForWindowsMS => PlayerName.SaltPlayerForWindowsMS,
PlayerId.SaltPlayerForWindowsSteam => PlayerName.SaltPlayerForWindowsSteam,
PlayerId.MoeKoeMusic => PlayerName.MoeKoeMusic,
PlayerId.MoeKoeMusicAlternative => PlayerName.MoeKoeMusic,
PlayerId.Listen1 => PlayerName.Listen1,
PlayerId.OriginalSoundHQPlayer => PlayerName.OriginalSoundHQPlayer,
_ => id,
};
public static string GetLogoPath(string? id) => id switch
{
PlayerID.Spotify => PathHelper.SpotifyLogoPath,
PlayerID.AppleMusic => PathHelper.AppleMusicLogoPath,
PlayerID.AppleMusicAlternative => PathHelper.AppleMusicLogoPath,
PlayerID.iTunes => PathHelper.iTunesLogoPath,
PlayerID.KugouMusic => PathHelper.KugouMusicLogoPath,
PlayerID.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath,
PlayerID.QQMusic => PathHelper.QQMusicLogoPath,
PlayerID.LXMusic => PathHelper.LXMusicLogoPath,
PlayerID.LXMusicPortable => PathHelper.LXMusicLogoPath,
PlayerID.MediaPlayerWindows11 => PathHelper.MediaPlayerWindows11LogoPath,
PlayerID.AIMP => PathHelper.AIMPLogoPath,
PlayerID.Foobar2000 => PathHelper.Foobar2000LogoPath,
PlayerID.MusicBee => PathHelper.MusicBeeLogoPath,
PlayerID.PotPlayer => PathHelper.PotPlayerLogoPath,
PlayerID.Chrome => PathHelper.ChromeLogoPath,
PlayerID.Edge => PathHelper.EdgeLogoPath,
PlayerID.BetterLyrics => PathHelper.LogoPath,
PlayerID.BetterLyricsDebug => PathHelper.LogoPath,
PlayerID.SaltPlayerForWindowsMS => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.SaltPlayerForWindowsSteam => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerID.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
PlayerID.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
PlayerID.Listen1 => PathHelper.Listen1LogoPath,
PlayerId.Spotify => PathHelper.SpotifyLogoPath,
PlayerId.AppleMusic => PathHelper.AppleMusicLogoPath,
PlayerId.AppleMusicAlternative => PathHelper.AppleMusicLogoPath,
PlayerId.iTunes => PathHelper.iTunesLogoPath,
PlayerId.KugouMusic => PathHelper.KugouMusicLogoPath,
PlayerId.NetEaseCloudMusic => PathHelper.NetEaseCloudMusicLogoPath,
PlayerId.QQMusic => PathHelper.QQMusicLogoPath,
PlayerId.LXMusic => PathHelper.LXMusicLogoPath,
PlayerId.LXMusicPortable => PathHelper.LXMusicLogoPath,
PlayerId.MediaPlayerWindows11 => PathHelper.MediaPlayerWindows11LogoPath,
PlayerId.AIMP => PathHelper.AIMPLogoPath,
PlayerId.Foobar2000 => PathHelper.Foobar2000LogoPath,
PlayerId.MusicBee => PathHelper.MusicBeeLogoPath,
PlayerId.PotPlayer => PathHelper.PotPlayerLogoPath,
PlayerId.Chrome => PathHelper.ChromeLogoPath,
PlayerId.Edge => PathHelper.EdgeLogoPath,
PlayerId.BetterLyrics => PathHelper.LogoPath,
PlayerId.BetterLyricsDebug => PathHelper.LogoPath,
PlayerId.SaltPlayerForWindowsMS => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerId.SaltPlayerForWindowsSteam => PathHelper.SaltPlayerForWindowsLogoPath,
PlayerId.MoeKoeMusic => PathHelper.MoeKoeMusicLogoPath,
PlayerId.MoeKoeMusicAlternative => PathHelper.MoeKoeMusicLogoPath,
PlayerId.Listen1 => PathHelper.Listen1LogoPath,
PlayerId.OriginalSoundHQPlayer => PathHelper.OriginalSoundHQPlayerLogoPath,
_ => PathHelper.UnknownPlayerLogoPath,
};
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class WebDavProbeHelper
{
/// <summary>
/// 自动检测目标主机是 HTTP 还是 HTTPS
/// </summary>
/// <returns>返回 "https" 或 "http",如果都连不上返回 null</returns>
public static async Task<string?> DetectSchemeAsync(string host, int port, string? path, string? user, string? pwd)
{
if (port == 443) return "https";
if (port == 80) return "http";
// 忽略 SSL 证书错误,因为很多 NAS 是自签名的
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true,
UseProxy = false
};
// 设置认证
if (!string.IsNullOrEmpty(user) && !string.IsNullOrEmpty(pwd))
{
handler.Credentials = new NetworkCredential(user, pwd);
handler.PreAuthenticate = true;
}
using var client = new HttpClient(handler);
client.Timeout = TimeSpan.FromSeconds(3);
if (await ProbeUrlAsync(client, "https", host, port, path))
{
return "https";
}
if (await ProbeUrlAsync(client, "http", host, port, path))
{
return "http";
}
// 都失败
return null;
}
private static async Task<bool> ProbeUrlAsync(HttpClient client, string scheme, string host, int port, string? path)
{
try
{
var uriBuilder = new UriBuilder(scheme, host, port, path);
// 使用 PROPFIND 方法,且 Depth 为 0只检测根节点是否存在不拉取列表
var request = new HttpRequestMessage(new HttpMethod("PROPFIND"), uriBuilder.Uri);
request.Headers.Add("Depth", "0");
var response = await client.SendAsync(request);
return response.StatusCode != HttpStatusCode.BadRequest;
}
catch
{
return false;
}
}
}
}

View File

@@ -17,12 +17,20 @@ namespace BetterLyrics.WinUI3.Hooks
static SystemVolumeHook()
{
_deviceEnumerator = new MMDeviceEnumerator();
_defaultDevice = _deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
if (_defaultDevice != null)
try
{
_defaultDevice.AudioEndpointVolume.OnVolumeNotification += AudioEndpointVolume_OnVolumeNotification;
_deviceEnumerator = new MMDeviceEnumerator();
// 找不到设备会抛出异常,在这里截获它
_defaultDevice = _deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
if (_defaultDevice != null)
{
_defaultDevice.AudioEndpointVolume.OnVolumeNotification += AudioEndpointVolume_OnVolumeNotification;
}
}
catch (Exception ex)
{
_defaultDevice = null;
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Db
{
public partial class FilesIndexDbContext : DbContext
{
public FilesIndexDbContext(DbContextOptions<FilesIndexDbContext> options) : base(options) { }
public DbSet<FilesIndexItem> FilesIndex { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Db
{
public partial class PlayHistoryDbContext : DbContext
{
public PlayHistoryDbContext(DbContextOptions<PlayHistoryDbContext> options) : base(options) { }
public DbSet<PlayHistoryItem> PlayHistory { get; set; }
}
}

View File

@@ -136,7 +136,7 @@ namespace BetterLyrics.WinUI3.Models
SetFromTrack(track);
}
public ExtendedTrack(FileCacheEntity? entity, Stream? stream = null) : base()
public ExtendedTrack(FilesIndexItem? entity, Stream? stream = null) : base()
{
if (entity == null) return;

View File

@@ -1,57 +0,0 @@
using SQLite;
using System;
namespace BetterLyrics.WinUI3.Models
{
[Table("FileCache")]
public class FileCacheEntity
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
// 【新增】关键字段!
// 关联到 MediaFolder.Id。
// 作用:
// 1. 区分不同配置(即使两个配置连的是同一个 SMB但在 APP 里视为不同源)。
// 2. 删除配置时,可以由 MediaFolderId 快速级联删除所有缓存。
[Indexed]
public string MediaFolderId { get; set; }
// 【修改】从 ParentPath 改为 ParentUri
// 存储父文件夹的标准 URI (smb://host/share/parent)
// 根目录文件的 ParentUri 可以为空,或者等于 MediaFolder 的 Base Uri
[Indexed]
public string? ParentUri { get; set; }
// 【核心】标准化的完整 URI (smb://host/share/folder/file.ext)
// 确保它是 URL 编码过且格式统一的
[Indexed(Unique = true)]
public string Uri { get; set; }
public string FileName { get; set; } = "";
public bool IsDirectory { get; set; }
// 记录文件大小,同步时用来对比文件是否变化
public long FileSize { get; set; }
// 记录修改时间,同步时对比使用
public DateTime? LastModified { get; set; }
// ------ 元数据部分 (保持不变) ------
public string Title { get; set; } = "";
public string Artists { get; set; } = "";
public string Album { get; set; } = "";
public int? Year { get; set; }
public int Bitrate { get; set; }
public double SampleRate { get; set; }
public int BitDepth { get; set; }
public int Duration { get; set; }
public string AudioFormatName { get; set; } = "";
public string AudioFormatShortName { get; set; } = "";
public string Encoder { get; set; } = "";
public string? EmbeddedLyrics { get; set; }
public string? LocalAlbumArtPath { get; set; }
public bool IsMetadataParsed { get; set; }
}
}

View File

@@ -0,0 +1,67 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BetterLyrics.WinUI3.Models
{
[Index(nameof(MediaFolderId))] // 普通索引
[Index(nameof(ParentUri))] // 普通索引
[Index(nameof(Uri), IsUnique = true)] // 唯一索引
public class FilesIndexItem
{
[Key] // 主键
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // 明确指定为自增 (Identity)
public int Id { get; set; }
// 关联到 MediaFolder.Id
// 注意:作为索引列,必须限制长度,否则 SQL Server 会报错 (索引最大900字节)
[MaxLength(450)]
public string MediaFolderId { get; set; }
// 存储父文件夹的标准 URI
// 允许为空
[MaxLength(450)]
public string? ParentUri { get; set; }
// 唯一索引列
// 必须限制长度。450字符 * 2字节/字符 = 900字节 (正好卡在 SQL Server 限制内)
[Required]
[MaxLength(450)]
public string Uri { get; set; }
public string FileName { get; set; } = "";
public bool IsDirectory { get; set; }
public long FileSize { get; set; }
public DateTime? LastModified { get; set; }
// 下面的元数据字段通常不需要索引,可以使用 MaxLength 稍微优化空间,
// 或者直接留空(默认为 nvarchar(max)
public string Title { get; set; } = "";
public string Artists { get; set; } = "";
public string Album { get; set; } = "";
public int? Year { get; set; }
public int Bitrate { get; set; }
public double SampleRate { get; set; }
public int BitDepth { get; set; }
public int Duration { get; set; }
[MaxLength(50)] // 格式名称通常很短,限制一下是个好习惯
public string AudioFormatName { get; set; } = "";
[MaxLength(20)]
public string AudioFormatShortName { get; set; } = "";
public string Encoder { get; set; } = "";
// 歌词可能会很长,保留默认的 nvarchar(max) 即可
public string? EmbeddedLyrics { get; set; }
public string? LocalAlbumArtPath { get; set; }
public bool IsMetadataParsed { get; set; }
}
}

View File

@@ -3,6 +3,7 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.FileSystemService.Providers;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Text.Json.Serialization;
using System.Threading;
@@ -30,6 +31,7 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty][NotifyPropertyChangedFor(nameof(UriString))] public partial string UriHost { get; set; }
[ObservableProperty][NotifyPropertyChangedFor(nameof(UriString))] public partial int UriPort { get; set; } = -1;
[JsonPropertyName("Path")]
[ObservableProperty]
[NotifyPropertyChangedRecipients]
[NotifyPropertyChangedFor(nameof(ConnectionSummary))]
@@ -40,12 +42,10 @@ namespace BetterLyrics.WinUI3.Models
[JsonIgnore] public bool IsLocal => SourceType == FileSourceType.Local;
[JsonIgnore][ObservableProperty] public partial bool IsIndexing { get; set; } = false;
[JsonIgnore][ObservableProperty] public partial double IndexingProgress { get; set; } = 0;
[JsonIgnore][ObservableProperty] public partial string IndexingStatusText { get; set; } = "";
[JsonIgnore][ObservableProperty] public partial bool IsCleaningUp { get; set; } = false;
[JsonIgnore][ObservableProperty] public partial string CleaningUpStatusText { get; set; } = "";
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsProcessing { get; set; } = false;
[ObservableProperty] public partial double IndexingProgress { get; set; } = 0;
[ObservableProperty] public partial string StatusText { get; set; } = "";
[ObservableProperty] public partial InfoBarSeverity StatusSeverity { get; set; } = InfoBarSeverity.Informational;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial DateTime? LastSyncTime { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AutoScanInterval ScanInterval { get; set; } = AutoScanInterval.Disabled;
@@ -118,7 +118,7 @@ namespace BetterLyrics.WinUI3.Models
FileSourceType.Local => new LocalFileSystem(this),
FileSourceType.SMB => new SMBFileSystem(this),
FileSourceType.FTP => new FTPFileSystem(this),
FileSourceType.WebDav => new WebDavFileSystem(this),
FileSourceType.WebDAV => new WebDavFileSystem(this),
_ => throw new NotImplementedException()
};
}

View File

@@ -35,11 +35,11 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsSearchType LyricsSearchType { get; set; } = LyricsSearchType.Sequential;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int MatchingThreshold { get; set; } = 40;
[JsonIgnore] public string LogoPath => PlayerIDHelper.GetLogoPath(Provider);
[JsonIgnore] public string LogoPath => PlayerIdHelper.GetLogoPath(Provider);
[JsonIgnore] public string? DisplayName => PlayerIDHelper.GetDisplayName(Provider);
[JsonIgnore] public string? DisplayName => PlayerIdHelper.GetDisplayName(Provider);
[JsonIgnore] public bool IsLXMusic => PlayerIDHelper.IsLXMusic(Provider);
[JsonIgnore] public bool IsLXMusic => PlayerIdHelper.IsLXMusic(Provider);
public MediaSourceProviderInfo()
{
@@ -53,7 +53,7 @@ namespace BetterLyrics.WinUI3.Models
IsEnabled = isEnable;
switch (provider)
{
case Constants.PlayerID.AppleMusic:
case Constants.PlayerId.AppleMusic:
// Apple Music 的特性
TimelineSyncThreshold = 1000;
PositionOffset = 1000;

View File

@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BetterLyrics.WinUI3.Models
{
[Index(nameof(Title))]
[Index(nameof(Artist))]
[Index(nameof(StartedAt))] // 用于按时间排序查询(如:最近播放)
[Index(nameof(PlayerId))]
public class PlayHistoryItem
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // AutoIncrement
public int Id { get; set; }
// 注意:作为索引列,必须加 MaxLength。
// 如果不加,默认为 nvarchar(max)SQL Server 无法对其建立高效索引。
[MaxLength(450)]
public string Title { get; set; } = "";
[MaxLength(450)]
public string Artist { get; set; } = "";
// Album 没有索引,可以不限制长度,或者为了规范也限制一下
public string Album { get; set; } = "";
public DateTime StartedAt { get; set; }
public double DurationPlayedMs { get; set; }
public double TotalDurationMs { get; set; }
// PlayerId 通常是个 GUID 或者短字符串,给 100 长度通常足够了,节省索引空间
[MaxLength(100)]
public string PlayerId { get; set; } = "";
}
}

View File

@@ -0,0 +1,15 @@
using BetterLyrics.WinUI3.Helper;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models
{
public class PlayerStatDisplayItem
{
public string PlayerId { get; set; }
public int PlayCount { get; set; }
public string PlayerName => PlayerIdHelper.GetDisplayName(PlayerId);
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Stats
{
public class ArtistPlayCount
{
public string Artist { get; set; }
public int PlayCount { get; set; }
public double TotalDurationSeconds { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Stats
{
public class PlayerStats
{
public string PlayerId { get; set; }
public int Count { get; set; }
public double DisplayWidth => (TotalCount > 0) ? (Count / (double)TotalCount) * 150 : 0;
public int TotalCount { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Stats
{
public class SongPlayCount
{
public string Title { get; set; }
public string Artist { get; set; }
public int PlayCount { get; set; }
}
}

View File

@@ -90,7 +90,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
allFiles = allFiles.Where(x => FileHelper.MusicExtensions.Contains(Path.GetExtension(x.FileName))).ToList();
FileCacheEntity? bestMatch = null;
FilesIndexItem? bestMatch = null;
foreach (var item in allFiles)
{

View File

@@ -1,14 +1,16 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Services.FileSystemService.Providers;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SQLite;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -27,7 +29,8 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
private readonly ILocalizationService _localizationService;
private readonly ILogger<FileSystemService> _logger;
private readonly SQLiteAsyncConnection _db;
private readonly IDbContextFactory<FilesIndexDbContext> _contextFactory;
private bool _isInitialized = false;
// 定时器字典
@@ -35,54 +38,37 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
// 当前正在执行的扫描任务字典
private readonly ConcurrentDictionary<string, CancellationTokenSource> _activeScanTokens = new();
private static readonly SemaphoreSlim _dbLock = new(1, 1);
private static readonly SemaphoreSlim _folderScanLock = new(1, 1);
public FileSystemService(ISettingsService settingsService, ILocalizationService localizationService, ILogger<FileSystemService> logger)
public FileSystemService(
ISettingsService settingsService,
ILocalizationService localizationService,
ILogger<FileSystemService> logger,
IDbContextFactory<FilesIndexDbContext> contextFactory)
{
_logger = logger;
_localizationService = localizationService;
_settingsService = settingsService;
_db = new SQLiteAsyncConnection(PathHelper.FilesCachePath);
_contextFactory = contextFactory;
}
public async Task InitializeAsync()
public async Task<List<FilesIndexItem>> GetFilesAsync(IUnifiedFileSystem provider, FilesIndexItem? parentFolder, string configId, bool forceRefresh = false)
{
if (_isInitialized) return;
string queryParentUri = parentFolder == null ? "" : parentFolder.Uri;
if (parentFolder == null && !forceRefresh) forceRefresh = true;
await _db.CreateTableAsync<FileCacheEntity>();
using var context = await _contextFactory.CreateDbContextAsync();
_isInitialized = true;
}
public async Task<List<FileCacheEntity>> GetFilesAsync(IUnifiedFileSystem provider, FileCacheEntity? parentFolder, string configId, bool forceRefresh = false)
{
await InitializeAsync();
string queryParentUri;
if (parentFolder == null)
{
if (!forceRefresh) forceRefresh = true;
queryParentUri = "";
}
else
{
queryParentUri = parentFolder.Uri;
}
List<FileCacheEntity> cachedEntities = new List<FileCacheEntity>();
if (parentFolder != null)
{
cachedEntities = await _db.Table<FileCacheEntity>()
.Where(x => x.MediaFolderId == configId && x.ParentUri == queryParentUri)
.ToListAsync();
}
var cachedEntities = await context.FilesIndex
.AsNoTracking() // 读操作不追踪,提升性能
.Where(x => x.MediaFolderId == configId && x.ParentUri == queryParentUri)
.ToListAsync();
bool needSync = forceRefresh || cachedEntities.Count == 0;
if (needSync)
{
// SyncAsync 内部自己管理 Context
cachedEntities = await SyncAsync(provider, parentFolder, configId);
}
@@ -90,17 +76,11 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
}
/// <summary>
/// 从远端/本地同步文件至数据库,该阶段不会解析文件全部元数据。
/// <para/>
/// 如果某个已有文件被修改或有新文件被添加,会预留空位,等待后续填充(通常交给 <see cref="ScanMediaFolderAsync"/> 完成)
/// 从远端/本地同步文件至数据库
/// </summary>
/// <param name="provider"></param>
/// <param name="parentFolder"></param>
/// <param name="configId"></param>
/// <returns></returns>
private async Task<List<FileCacheEntity>> SyncAsync(IUnifiedFileSystem provider, FileCacheEntity? parentFolder, string configId)
private async Task<List<FilesIndexItem>> SyncAsync(IUnifiedFileSystem provider, FilesIndexItem? parentFolder, string configId)
{
List<FileCacheEntity> remoteItems;
List<FilesIndexItem> remoteItems;
try
{
remoteItems = await provider.GetFilesAsync(parentFolder);
@@ -115,80 +95,79 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
string targetParentUri = "";
if (remoteItems.Count > 0)
{
targetParentUri = remoteItems[0].ParentUri ?? "";
}
else if (parentFolder != null)
{
targetParentUri = parentFolder.Uri;
}
else
{
return [];
}
try
{
await _db.RunInTransactionAsync(conn =>
using var context = await _contextFactory.CreateDbContextAsync();
// 开启事务 (EF Core 也能管理事务)
using var transaction = await context.Database.BeginTransactionAsync();
// 1. 获取数据库中现有的该目录下的文件
var dbItems = await context.FilesIndex
.Where(x => x.MediaFolderId == configId && x.ParentUri == targetParentUri)
.ToListAsync();
var dbMap = dbItems.ToDictionary(x => x.Uri, x => x);
// 2. 远端数据去重(防止 Provider 返回重复 Uri
var remoteDistinct = remoteItems
.GroupBy(x => x.Uri)
.Select(g => g.First())
.ToList();
var remoteUris = new HashSet<string>();
// 3. 处理 新增 和 更新
foreach (var remote in remoteDistinct)
{
var dbItems = conn.Table<FileCacheEntity>()
.Where(x => x.MediaFolderId == configId && x.ParentUri == targetParentUri)
.ToList();
remoteUris.Add(remote.Uri);
var dbMap = dbItems.ToDictionary(x => x.Uri, x => x);
var remoteMap = remoteItems
.GroupBy(x => x.Uri)
.Select(g => g.First())
.ToDictionary(x => x.Uri, x => x);
var toInsert = new List<FileCacheEntity>();
var toUpdate = new List<FileCacheEntity>();
var toDelete = new List<FileCacheEntity>();
foreach (var remote in remoteItems)
if (dbMap.TryGetValue(remote.Uri, out var existing))
{
if (dbMap.TryGetValue(remote.Uri, out var existing))
{
bool isChanged = existing.FileSize != remote.FileSize ||
existing.LastModified != remote.LastModified;
// 检查是否变更
bool isChanged = existing.FileSize != remote.FileSize ||
existing.LastModified != remote.LastModified;
if (isChanged)
{
existing.FileSize = remote.FileSize;
existing.LastModified = remote.LastModified;
existing.IsMetadataParsed = false; // 标记为未解析,下次会重新读取元数据
toUpdate.Add(existing);
}
else
{
// 数据库里原有的 Title, Artist, LocalAlbumArtPath 都会被完美保留
}
}
else
if (isChanged)
{
toInsert.Add(remote);
existing.FileSize = remote.FileSize;
existing.LastModified = remote.LastModified;
existing.IsMetadataParsed = false; // 标记重新解析
// EF Core 自动追踪 existing 的变化,无需手动 Update
}
}
foreach (var dbItem in dbItems)
else
{
if (!remoteMap.ContainsKey(dbItem.Uri))
{
toDelete.Add(dbItem);
}
// 新增
// 注意:如果 Id 是自增的,不要手动赋值 Id除非是 Guid
context.FilesIndex.Add(remote);
}
}
if (toInsert.Count > 0) conn.InsertAll(toInsert);
if (toUpdate.Count > 0) conn.UpdateAll(toUpdate);
if (toDelete.Count > 0)
// 4. 处理 删除 (数据库有,远端没有)
foreach (var dbItem in dbItems)
{
if (!remoteUris.Contains(dbItem.Uri))
{
foreach (var item in toDelete) conn.Delete(item);
context.FilesIndex.Remove(dbItem);
}
});
}
var finalItems = await _db.Table<FileCacheEntity>()
await context.SaveChangesAsync();
await transaction.CommitAsync();
// 5. 返回最新数据
// 这里的 dbItems 已经被 Update 更新了内存状态,但 Remove 的还在列表里Add 的不在列表里
// 所以最稳妥的是重新查一次,或者手动维护列表。为了准确性,重新查询 (AsNoTracking)
var finalItems = await context.FilesIndex
.AsNoTracking()
.Where(x => x.MediaFolderId == configId && x.ParentUri == targetParentUri)
.ToListAsync();
@@ -203,37 +182,34 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
}
}
public async Task UpdateMetadataAsync(FileCacheEntity entity)
public async Task UpdateMetadataAsync(FilesIndexItem entity)
{
// 现在的实体已经包含了完整信息,直接 Update 即可
// 我们只需要确保 Where 子句用的是主键或者 Uri
using var context = await _contextFactory.CreateDbContextAsync();
// 简化版 SQL直接用 ORM 的 Update
// 但因为 entity 对象可能包含一些不应该被覆盖的旧数据(如果多线程操作),
// 手写 SQL 只更新 Metadata 字段更安全。
string sql = @"
UPDATE FileCache
SET
Title = ?, Artists = ?, Album = ?,
Year = ?, Bitrate = ?, SampleRate = ?, BitDepth = ?,
Duration = ?, AudioFormatName = ?, AudioFormatShortName = ?, Encoder = ?,
EmbeddedLyrics = ?, LocalAlbumArtPath = ?,
IsMetadataParsed = 1
WHERE Id = ?"; // 推荐用 Id (主键) 最快,如果没有 Id 则用 Uri
await _db.ExecuteAsync(sql,
entity.Title, entity.Artists, entity.Album,
entity.Year, entity.Bitrate, entity.SampleRate, entity.BitDepth,
entity.Duration, entity.AudioFormatName, entity.AudioFormatShortName, entity.Encoder,
entity.EmbeddedLyrics, entity.LocalAlbumArtPath,
entity.Id // WHERE Id = ?
);
// 使用 EF Core 7.0+ 的 ExecuteUpdateAsync 高效更新
// 这会直接生成 UPDATE SQL不经过内存加载性能极高
await context.FilesIndex
.Where(x => x.Id == entity.Id) // 优先用 Id
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Title, entity.Title)
.SetProperty(p => p.Artists, entity.Artists)
.SetProperty(p => p.Album, entity.Album)
.SetProperty(p => p.Year, entity.Year)
.SetProperty(p => p.Bitrate, entity.Bitrate)
.SetProperty(p => p.SampleRate, entity.SampleRate)
.SetProperty(p => p.BitDepth, entity.BitDepth)
.SetProperty(p => p.Duration, entity.Duration)
.SetProperty(p => p.AudioFormatName, entity.AudioFormatName)
.SetProperty(p => p.AudioFormatShortName, entity.AudioFormatShortName)
.SetProperty(p => p.Encoder, entity.Encoder)
.SetProperty(p => p.EmbeddedLyrics, entity.EmbeddedLyrics)
.SetProperty(p => p.LocalAlbumArtPath, entity.LocalAlbumArtPath)
.SetProperty(p => p.IsMetadataParsed, true)
);
}
public async Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FileCacheEntity entity)
public async Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FilesIndexItem entity)
{
// 直接传递实体给 Provider
return await provider.OpenReadAsync(entity);
}
@@ -241,8 +217,10 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
{
_dispatcherQueue.TryEnqueue(() =>
{
folder.CleaningUpStatusText = _localizationService.GetLocalizedString("FileSystemServicePrepareToClean");
folder.IsCleaningUp = true;
folder.IndexingProgress = 0;
folder.StatusSeverity = InfoBarSeverity.Informational;
folder.StatusText = _localizationService.GetLocalizedString("FileSystemServicePrepareToClean");
folder.IsProcessing = true;
});
if (_folderTimerTokens.TryRemove(folder.Id, out var timerCts))
@@ -255,7 +233,6 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
if (_activeScanTokens.TryGetValue(folder.Id, out var activeScanCts))
{
activeScanCts.Cancel();
// 强制终止正在扫描的操作
}
try
@@ -266,20 +243,19 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
{
_dispatcherQueue.TryEnqueue(() =>
{
folder.CleaningUpStatusText = _localizationService.GetLocalizedString("FileSystemServiceCleaningCache");
folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceCleaningCache");
});
await InitializeAsync();
using var context = await _contextFactory.CreateDbContextAsync();
await _dbLock.WaitAsync();
try
await context.FilesIndex
.Where(x => x.MediaFolderId == folder.Id)
.ExecuteDeleteAsync();
// VACUUM 是 SQLite 特有的命令
if (context.Database.IsSqlite())
{
await _db.ExecuteAsync("DELETE FROM FileCache WHERE MediaFolderId = ?", folder.Id);
await _db.ExecuteAsync("VACUUM");
}
finally
{
_dbLock.Release();
await context.Database.ExecuteSqlRawAsync("VACUUM");
}
}
finally
@@ -295,8 +271,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
{
_dispatcherQueue.TryEnqueue(() =>
{
folder.CleaningUpStatusText = "";
folder.IsCleaningUp = false;
folder.IsProcessing = false;
folder.LastSyncTime = null;
});
}
@@ -311,30 +286,33 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
_dispatcherQueue.TryEnqueue(() =>
{
folder.IsIndexing = true;
folder.StatusSeverity = InfoBarSeverity.Informational;
folder.IsProcessing = true;
folder.IndexingProgress = 0;
folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceWaitingForScan");
folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceWaitingForScan");
});
try
{
await _folderScanLock.WaitAsync(scanCts.Token);
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceConnecting"));
await InitializeAsync();
_dispatcherQueue.TryEnqueue(() => folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceConnecting"));
using var fs = folder.CreateFileSystem();
if (fs == null || !await fs.ConnectAsync())
{
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceConnectFailed"));
_dispatcherQueue.TryEnqueue(() =>
{
folder.StatusSeverity = InfoBarSeverity.Error;
folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceConnectFailed");
});
return;
}
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = _localizationService.GetLocalizedString("FileSystemServiceFetchingFileList"));
_dispatcherQueue.TryEnqueue(() => folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceFetchingFileList"));
var filesToProcess = new List<FileCacheEntity>();
var foldersToScan = new Queue<FileCacheEntity?>();
var filesToProcess = new List<FilesIndexItem>();
var foldersToScan = new Queue<FilesIndexItem?>();
foldersToScan.Enqueue(null); // 根目录
while (foldersToScan.Count > 0)
@@ -342,7 +320,6 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
if (scanCts.Token.IsCancellationRequested) return;
var currentParent = foldersToScan.Dequeue();
var items = await GetFilesAsync(fs, currentParent, folder.Id, forceRefresh: true);
foreach (var item in items)
@@ -374,10 +351,10 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
if (current % 10 == 0 || current == total)
{
double progress = (double)current / total * 100;
_dispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
_dispatcherQueue.TryEnqueue(() =>
{
folder.IndexingProgress = progress;
folder.IndexingStatusText = $"{_localizationService.GetLocalizedString("FileSystemServiceParsing")} {current}/{total}";
folder.StatusText = $"{_localizationService.GetLocalizedString("FileSystemServiceParsing")} {current}/{total}";
});
}
@@ -407,10 +384,8 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
if (track.Duration > 0)
{
// 保存封面
string? artPath = await SaveAlbumArtToDiskAsync(track);
// 填充实体
item.Title = track.Title;
item.Artists = track.Artist;
item.Album = track.Album;
@@ -422,7 +397,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
item.AudioFormatName = track.AudioFormatName;
item.AudioFormatShortName = track.AudioFormatShortName;
item.Encoder = track.Encoder;
item.EmbeddedLyrics = track.RawLyrics; // 内嵌歌词
item.EmbeddedLyrics = track.RawLyrics;
item.LocalAlbumArtPath = artPath;
item.IsMetadataParsed = true;
}
@@ -434,7 +409,6 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
{
using var reader = new StreamReader(stream);
string content = await reader.ReadToEndAsync();
item.EmbeddedLyrics = content;
item.IsMetadataParsed = true;
}
@@ -442,15 +416,10 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
if (item.IsMetadataParsed)
{
await _dbLock.WaitAsync(token);
try
{
await UpdateMetadataAsync(item);
}
finally
{
_dbLock.Release();
}
// 更新操作:直接调用 UpdateMetadataAsync
// 此时不需要 _dbLock因为 UpdateMetadataAsync 内部会 CreateDbContextAsync
// 而 _folderScanLock 已经保证了当前文件夹扫描的独占性
await UpdateMetadataAsync(item);
}
}
catch (Exception ex)
@@ -461,6 +430,8 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
_dispatcherQueue.TryEnqueue(() =>
{
folder.StatusSeverity = InfoBarSeverity.Success;
folder.StatusText = _localizationService.GetLocalizedString("FileSystemServiceReady");
folder.LastSyncTime = DateTime.Now;
});
}
@@ -470,40 +441,41 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
}
catch (Exception ex)
{
_dispatcherQueue.TryEnqueue(() => folder.IndexingStatusText = ex.Message);
_dispatcherQueue.TryEnqueue(() =>
{
folder.StatusText = ex.Message;
folder.StatusSeverity = InfoBarSeverity.Error;
});
}
finally
{
_folderScanLock.Release();
_activeScanTokens.TryRemove(folder.Id, out _);
_dispatcherQueue.TryEnqueue(() =>
{
folder.IsIndexing = false;
folder.IndexingStatusText = "";
folder.IndexingProgress = 100;
folder.IsProcessing = false;
folder.IndexingProgress = 0;
});
}
}
public async Task<List<FileCacheEntity>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds)
public async Task<List<FilesIndexItem>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds)
{
await InitializeAsync();
if (enabledConfigIds == null || !enabledConfigIds.Any())
{
return new List<FileCacheEntity>();
return new List<FilesIndexItem>();
}
var idList = enabledConfigIds.ToList();
// SQL 逻辑: SELECT * FROM FileCache WHERE IsMetadataParsed = 1 AND MediaFolderId IN (...)
var results = await _db.Table<FileCacheEntity>()
using var context = await _contextFactory.CreateDbContextAsync();
// SQL: SELECT * FROM FileCache WHERE IsMetadataParsed = 1 AND MediaFolderId IN (...)
return await context.FilesIndex
.AsNoTracking()
.Where(x => x.IsMetadataParsed && idList.Contains(x.MediaFolderId))
.ToListAsync();
return results;
}
public void StartAllFolderTimers()
@@ -563,11 +535,11 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
}, newCts.Token);
}
// 参数为 string parentUri表示哪个文件夹的内容变了
public event EventHandler<string>? FolderUpdated;
private async Task<string?> SaveAlbumArtToDiskAsync(ExtendedTrack track)
{
// 代码未变,纯 IO 操作
var picData = track.AlbumArtByteArray;
if (picData == null || picData.Length == 0) return null;
@@ -620,6 +592,5 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
}
}
}
}
}

View File

@@ -10,12 +10,6 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
{
public interface IFileSystemService
{
/// <summary>
/// 初始化(连接)数据库
/// </summary>
/// <returns></returns>
Task InitializeAsync();
/// <summary>
/// 从数据库拉取文件(必要时需要从远端/本地同步至数据库)
/// </summary>
@@ -24,7 +18,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
/// <param name="configId"></param>
/// <param name="forceRefresh">强制需要从远端/本地同步至数据库</param>
/// <returns></returns>
Task<List<FileCacheEntity>> GetFilesAsync(IUnifiedFileSystem provider, FileCacheEntity? parentFolder, string configId, bool forceRefresh = false);
Task<List<FilesIndexItem>> GetFilesAsync(IUnifiedFileSystem provider, FilesIndexItem? parentFolder, string configId, bool forceRefresh = false);
/// <summary>
/// 打开文件(通过远端/本地流)
@@ -32,14 +26,14 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
/// <param name="provider"></param>
/// <param name="entity"></param>
/// <returns></returns>
Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FileCacheEntity entity);
Task<Stream?> OpenFileAsync(IUnifiedFileSystem provider, FilesIndexItem entity);
/// <summary>
/// 更新数据库(单个文件)
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
Task UpdateMetadataAsync(FileCacheEntity entity);
Task UpdateMetadataAsync(FilesIndexItem entity);
/// <summary>
/// 从数据库删除
@@ -60,7 +54,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
/// </summary>
/// <param name="enabledConfigIds"></param>
/// <returns></returns>
Task<List<FileCacheEntity>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds);
Task<List<FilesIndexItem>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds);
void StartAllFolderTimers();

View File

@@ -14,13 +14,13 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
/// </summary>
/// <param name="parentFolder"></param>
/// <returns></returns>
Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null);
Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null);
/// <summary>
/// 打开流
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
Task<Stream?> OpenReadAsync(FileCacheEntity file);
Task<Stream?> OpenReadAsync(FilesIndexItem file);
Task DisconnectAsync();
}
}

View File

@@ -42,22 +42,14 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
public async Task<bool> ConnectAsync()
{
try
{
if (_client.IsConnected) return true;
await _client.AutoConnect(); // AutoConnect 会自动尝试 FTP/FTPS
return _client.IsConnected;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"FTP连接失败: {ex.Message}");
return false;
}
if (_client.IsConnected) return true;
await _client.AutoConnect(); // AutoConnect 会自动尝试 FTP/FTPS
return _client.IsConnected;
}
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var result = new List<FileCacheEntity>();
var result = new List<FilesIndexItem>();
string targetServerPath;
Uri parentUri;
@@ -104,7 +96,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
Path = item.FullName
};
result.Add(new FileCacheEntity
result.Add(new FilesIndexItem
{
MediaFolderId = _config.Id,
// 如果是根目录扫描ParentUri 用 Config 的;否则用传入文件夹的
@@ -130,7 +122,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
return result;
}
public async Task<Stream?> OpenReadAsync(FileCacheEntity file)
public async Task<Stream?> OpenReadAsync(FilesIndexItem file)
{
if (file == null) return null;

View File

@@ -20,12 +20,20 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
public Task<bool> ConnectAsync()
{
return Task.FromResult(Directory.Exists(_rootLocalPath));
var isExisted = Directory.Exists(_rootLocalPath);
if (isExisted)
{
return Task.FromResult(true);
}
else
{
throw new FileNotFoundException(null, _rootLocalPath);
}
}
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var result = new List<FileCacheEntity>();
var result = new List<FilesIndexItem>();
string targetPath;
string parentUriString;
@@ -70,7 +78,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
size = fi.Length;
}
result.Add(new FileCacheEntity
result.Add(new FilesIndexItem
{
MediaFolderId = _config.Id, // 关联配置 ID
@@ -94,7 +102,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
return await Task.FromResult(result);
}
public async Task<Stream?> OpenReadAsync(FileCacheEntity entity)
public async Task<Stream?> OpenReadAsync(FilesIndexItem entity)
{
if (entity == null) return null;

View File

@@ -42,29 +42,22 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
public async Task<bool> ConnectAsync()
{
try
{
_client = new SMB2Client();
_client = new SMB2Client();
// 连接主机
bool connected = _client.Connect(_config.UriHost, SMBTransportType.DirectTCPTransport);
if (!connected) return false;
// 连接主机
bool connected = _client.Connect(_config.UriHost, SMBTransportType.DirectTCPTransport);
if (!connected) return false;
// 登录
var status = _client.Login(string.Empty, _config.UserName, _config.Password);
if (status != NTStatus.STATUS_SUCCESS) return false;
// 登录
var status = _client.Login(string.Empty, _config.UserName, _config.Password);
if (status != NTStatus.STATUS_SUCCESS) return false;
// 连接共享目录 (TreeConnect)
// SMBLibrary 必须先连接到 Share后续所有文件操作都是基于这个 Share 的相对路径
if (string.IsNullOrEmpty(_shareName)) return false;
// 连接共享目录 (TreeConnect)
// SMBLibrary 必须先连接到 Share后续所有文件操作都是基于这个 Share 的相对路径
if (string.IsNullOrEmpty(_shareName)) return false;
_fileStore = _client.TreeConnect(_shareName, out status);
return status == NTStatus.STATUS_SUCCESS;
}
catch (Exception)
{
return false;
}
_fileStore = _client.TreeConnect(_shareName, out status);
return status == NTStatus.STATUS_SUCCESS;
}
/// <summary>
@@ -74,9 +67,9 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
/// 传入要列出的文件夹实体。
/// 如果传入 null则默认列出 MediaFolder 配置的根目录。
/// </param>
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var result = new List<FileCacheEntity>();
var result = new List<FilesIndexItem>();
if (_fileStore == null) return result;
string smbPath = GetPathRelativeToShare(parentFolder);
@@ -128,7 +121,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
var baseUri = new Uri(parentUriString);
var newUri = new Uri(baseUri, item.FileName);
result.Add(new FileCacheEntity
result.Add(new FilesIndexItem
{
MediaFolderId = _config.Id,
ParentUri = parentFolder?.Uri ?? _config.GetStandardUri().AbsoluteUri,
@@ -155,7 +148,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
/// 打开文件流
/// </summary>
/// <param name="file">只需要传入文件实体即可</param>
public async Task<Stream?> OpenReadAsync(FileCacheEntity file)
public async Task<Stream?> OpenReadAsync(FilesIndexItem file)
{
if (_fileStore == null || file == null) return null;
@@ -181,7 +174,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
_client?.Disconnect();
}
private string GetPathRelativeToShare(FileCacheEntity? entity)
private string GetPathRelativeToShare(FilesIndexItem? entity)
{
Uri targetUri;

View File

@@ -36,22 +36,13 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
public async Task<bool> ConnectAsync()
{
try
{
// 测试连接Propfind 请求配置的根路径
// GetStandardUri 已经包含了用户设置的路径
var result = await _client.Propfind(_config.GetStandardUri().AbsoluteUri);
return result.IsSuccessful;
}
catch
{
return false;
}
var result = await _client.Propfind(_config.GetStandardUri().AbsoluteUri);
return result.IsSuccessful;
}
public async Task<List<FileCacheEntity>> GetFilesAsync(FileCacheEntity? parentFolder = null)
public async Task<List<FilesIndexItem>> GetFilesAsync(FilesIndexItem? parentFolder = null)
{
var list = new List<FileCacheEntity>();
var list = new List<FilesIndexItem>();
Uri targetUri;
if (parentFolder == null)
@@ -98,7 +89,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
if (string.IsNullOrEmpty(extension) || !FileHelper.AllSupportedExtensions.Contains(extension)) continue;
}
list.Add(new FileCacheEntity
list.Add(new FilesIndexItem
{
MediaFolderId = _config.Id,
@@ -118,7 +109,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService.Providers
return list;
}
public async Task<Stream?> OpenReadAsync(FileCacheEntity entity)
public async Task<Stream?> OpenReadAsync(FilesIndexItem entity)
{
if (entity == null) return null;

View File

@@ -279,7 +279,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
int maxScore = 0;
FileCacheEntity? bestFileEntity = null;
FilesIndexItem? bestFileEntity = null;
MediaFolder? bestFolderConfig = null;
var lyricsSearchResult = new LyricsSearchResult();
@@ -345,7 +345,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
var allFiles = await _fileSystemService.GetParsedFilesAsync(enabledIds);
allFiles = allFiles.Where(x => FileHelper.MusicExtensions.Contains(Path.GetExtension(x.FileName))).ToList();
FileCacheEntity? bestFile = null;
FilesIndexItem? bestFile = null;
int maxScore = 0;
foreach (var item in allFiles)
@@ -558,11 +558,11 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
ISearchResult? result;
if (songInfo.SongId != null && searcher == Searchers.Netease && PlayerIDHelper.IsNeteaseFamily(songInfo.PlayerId))
if (songInfo.SongId != null && searcher == Searchers.Netease && PlayerIdHelper.IsNeteaseFamily(songInfo.PlayerId))
{
result = new NeteaseSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId);
}
else if (songInfo.SongId != null && searcher == Searchers.QQMusic && songInfo.PlayerId == Constants.PlayerID.QQMusic)
else if (songInfo.SongId != null && searcher == Searchers.QQMusic && songInfo.PlayerId == Constants.PlayerId.QQMusic)
{
result = new QQMusicSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId, "");
}

View File

@@ -11,7 +11,9 @@ using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslationService;
using BetterLyrics.WinUI3.Services.TransliterationService;
@@ -54,6 +56,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private readonly ITransliterationService _transliterationService;
private readonly ISettingsService _settingsService;
private readonly IDiscordService _discordService;
private readonly IPlayHistoryService _playHistoryService;
private readonly ILastFMService _lastFMService;
private readonly ILogger<MediaSessionsService> _logger;
private double _lxMusicPositionSeconds = 0;
@@ -61,6 +65,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private readonly DispatcherQueueTimer? _onMediaPropsChangedTimer;
private readonly Stopwatch _scrobbleStopwatch = new();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool CurrentIsPlaying { get; private set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TimeSpan CurrentPosition { get; private set; } = TimeSpan.Zero;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial SongInfo? CurrentSongInfo { get; private set; } = SongInfoExtensions.Placeholder;
@@ -74,6 +80,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
IDiscordService discordService,
ITranslationService libreTranslateService,
ITransliterationService transliterationService,
IPlayHistoryService playHistoryService,
ILastFMService lastFMService,
ILogger<MediaSessionsService> logger)
{
_settingsService = settingsService;
@@ -82,6 +90,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_translationService = libreTranslateService;
_transliterationService = transliterationService;
_discordService = discordService;
_playHistoryService = playHistoryService;
_lastFMService = lastFMService;
_logger = logger;
_onMediaPropsChangedTimer = _dispatcherQueue.CreateTimer();
@@ -144,7 +154,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
var found = _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(s => s.Provider == id);
if (_settingsService.AppSettings.MusicGallerySettings.LyricsWindowStatus.IsOpened)
{
if (PlayerIDHelper.IsBetterLyrics(found?.Provider))
if (PlayerIdHelper.IsBetterLyrics(found?.Provider))
{
return found?.IsEnabled ?? true;
}
@@ -193,6 +203,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
return;
}
@@ -203,6 +214,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (!IsMediaSourceEnabled(mediaSession.Id))
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
}
else
@@ -229,7 +241,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
var desiredSession = GetCurrentSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
@@ -244,6 +255,15 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_ => false,
};
}
if (CurrentIsPlaying)
{
_scrobbleStopwatch.Start();
}
else
{
_scrobbleStopwatch.Stop();
}
}));
}
@@ -269,7 +289,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
if (PlayerIDHelper.IsLXMusic(sessionId))
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StopSSE();
}
@@ -288,20 +308,20 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
string? fixedAlbum = mediaProperties?.AlbumTitle;
string? songId = null;
if (PlayerIDHelper.IsAppleMusic(sessionId))
if (PlayerIdHelper.IsAppleMusic(sessionId))
{
fixedArtist = mediaProperties?.Artist.Split(" — ").FirstOrDefault();
fixedAlbum = mediaProperties?.Artist.Split(" — ").LastOrDefault();
fixedAlbum = fixedAlbum?.Replace(" - Single", "");
fixedAlbum = fixedAlbum?.Replace(" - EP", "");
}
else if (PlayerIDHelper.IsNeteaseFamily(sessionId))
else if (PlayerIdHelper.IsNeteaseFamily(sessionId))
{
songId = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.NetEaseCloudMusicTrackID))?
.Replace(ExtendedGenreFiled.NetEaseCloudMusicTrackID, "");
}
else if (sessionId == PlayerID.QQMusic)
else if (sessionId == PlayerId.QQMusic)
{
songId = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.QQMusicTrackID))?
@@ -312,6 +332,35 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.FileName))?
.Replace(ExtendedGenreFiled.FileName, "");
// 写入播放记录
if (CurrentSongInfo != null && CurrentSongInfo.Title != "N/A")
{
// 必须捕获一个副本给异步任务,因为 CurrentSongInfo 马上就要变了
var lastSong = CurrentSongInfo;
// 当前秒表时间 >= 上一首总时长 / 2
if (lastSong.DurationMs > 0 &&
_scrobbleStopwatch.Elapsed.TotalMilliseconds >= (lastSong.DurationMs / 2))
{
// 写入本地播放记录
var playHistoryItem = lastSong.ToPlayHistoryItem(_scrobbleStopwatch.Elapsed.TotalMilliseconds);
if (playHistoryItem != null)
{
// 后台
_ = Task.Run(() => _playHistoryService.AddLogAsync(playHistoryItem));
_logger.LogInformation($"[Scrobble] 结算成功: {lastSong.Title}");
}
// 写入 Last.fm 播放记录
var isLastFMEnabled = CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (isLastFMEnabled)
{
// 后台
_ = Task.Run(() => _lastFMService.TrackAsync(lastSong));
}
}
}
_scrobbleStopwatch.Restart();
CurrentSongInfo = new SongInfo
{
Title = mediaProperties?.Title ?? "N/A",
@@ -323,7 +372,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
LinkedFileName = linkedFileName
};
if (PlayerIDHelper.IsLXMusic(sessionId))
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StartSSE();
}
@@ -332,7 +381,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
StopSSE();
}
if (PlayerIDHelper.IsLXMusic(sessionId) && _lxMusicAlbumArtBytes != null)
if (PlayerIdHelper.IsLXMusic(sessionId) && _lxMusicAlbumArtBytes != null)
{
_SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer();
}
@@ -430,6 +479,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
CurrentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
_discordService.Disable();
@@ -523,7 +573,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
if (PlayerIDHelper.IsLXMusic(CurrentSongInfo?.PlayerId))
if (PlayerIdHelper.IsLXMusic(CurrentSongInfo?.PlayerId))
{
var data = JsonSerializer.Deserialize(e.Message, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.ValueKind == JsonValueKind.Number)

View File

@@ -0,0 +1,25 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Stats;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.PlayHistoryService
{
public interface IPlayHistoryService
{
Task AddLogAsync(PlayHistoryItem item);
Task<List<PlayHistoryItem>> GetRecentLogsAsync(int limit = 50);
Task<List<PlayHistoryItem>> GetLogsByDateRangeAsync(DateTime start, DateTime end);
Task<List<SongPlayCount>> GetTopSongsAsync(DateTime start, DateTime end, int limit = 10);
Task<List<ArtistPlayCount>> GetTopArtistsAsync(DateTime start, DateTime end, int limit = 10);
Task<TimeSpan> GetTotalListeningDurationAsync(DateTime start, DateTime end);
Task<List<PlayerStats>> GetPlayerDistributionAsync(DateTime start, DateTime end);
Task DeleteLogAsync(int id);
Task ClearHistoryAsync();
Task GenerateTestDataAsync(int count = 100);
}
}

View File

@@ -0,0 +1,258 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Models.Stats;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.PlayHistoryService
{
public class PlayHistoryService : IPlayHistoryService
{
private readonly IDbContextFactory<PlayHistoryDbContext> _contextFactory;
public PlayHistoryService(IDbContextFactory<PlayHistoryDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task AddLogAsync(PlayHistoryItem item)
{
using var context = await _contextFactory.CreateDbContextAsync();
// 确保 UTC
if (item.StartedAt.Kind != DateTimeKind.Utc)
{
item.StartedAt = item.StartedAt.ToUniversalTime();
}
context.PlayHistory.Add(item);
await context.SaveChangesAsync();
}
public async Task<List<PlayHistoryItem>> GetRecentLogsAsync(int limit = 50)
{
using var context = await _contextFactory.CreateDbContextAsync();
return await context.PlayHistory
.AsNoTracking() // 读操作,不需要追踪状态,提升性能
.OrderByDescending(x => x.StartedAt)
.Take(limit)
.ToListAsync();
}
public async Task<List<PlayHistoryItem>> GetLogsByDateRangeAsync(DateTime start, DateTime end)
{
using var context = await _contextFactory.CreateDbContextAsync();
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.ToListAsync();
}
public async Task<List<SongPlayCount>> GetTopSongsAsync(DateTime start, DateTime end, int limit = 10)
{
using var context = await _contextFactory.CreateDbContextAsync();
// EF Core 会自动将这个 LINQ 翻译成高效的 GROUP BY SQL
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.GroupBy(x => new { x.Title, x.Artist }) // 组合分组
.Select(g => new SongPlayCount
{
Title = g.Key.Title,
Artist = g.Key.Artist,
PlayCount = g.Count()
})
.OrderByDescending(x => x.PlayCount)
.Take(limit)
.ToListAsync();
}
public async Task<List<ArtistPlayCount>> GetTopArtistsAsync(DateTime start, DateTime end, int limit = 10)
{
using var context = await _contextFactory.CreateDbContextAsync();
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.GroupBy(x => x.Artist)
.Select(g => new ArtistPlayCount
{
Artist = g.Key,
PlayCount = g.Count(),
// 注意SQLite 存储 double 精度,这里求和后转秒
TotalDurationSeconds = g.Sum(x => x.DurationPlayedMs) / 1000.0
})
.OrderByDescending(x => x.PlayCount)
.Take(limit)
.ToListAsync();
}
public async Task<TimeSpan> GetTotalListeningDurationAsync(DateTime start, DateTime end)
{
using var context = await _contextFactory.CreateDbContextAsync();
var totalMs = await context.PlayHistory
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.SumAsync(x => Math.Min(x.DurationPlayedMs, x.TotalDurationMs)); // 防止超过歌曲本身时长
return TimeSpan.FromMilliseconds(totalMs);
}
public async Task<List<PlayerStats>> GetPlayerDistributionAsync(DateTime start, DateTime end)
{
using var context = await _contextFactory.CreateDbContextAsync();
return await context.PlayHistory
.AsNoTracking()
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.GroupBy(x => x.PlayerId)
.Select(g => new PlayerStats
{
PlayerId = g.Key,
Count = g.Count()
})
.OrderByDescending(x => x.Count)
.ToListAsync();
}
public async Task DeleteLogAsync(int id)
{
using var context = await _contextFactory.CreateDbContextAsync();
// EF Core 删除需要先查询,或者使用 ExecuteDeleteAsync (EF Core 7+)
// 写法 1 (传统):
// var item = await context.PlayHistory.FindAsync(id);
// if (item != null) { context.PlayHistory.Remove(item); await context.SaveChangesAsync(); }
// 写法 2 (EF Core 7.0+ 高效写法,直接生成 DELETE SQL):
await context.PlayHistory
.Where(x => x.Id == id)
.ExecuteDeleteAsync();
}
public async Task ClearHistoryAsync()
{
using var context = await _contextFactory.CreateDbContextAsync();
// 高效清空表
await context.PlayHistory.ExecuteDeleteAsync();
}
public async Task GenerateTestDataAsync(int count = 100)
{
// 这里的逻辑稍微重构了一下,使用批量插入提升性能
var random = new Random();
var presetSongs = new List<(string Title, string Artist, string Album)>
{
("Anti-Hero", "Taylor Swift", "Midnights"),
("Cruel Summer", "Taylor Swift", "Lover"),
("Blank Space", "Taylor Swift", "1989"),
("As It Was", "Harry Styles", "Harry's House"),
("Late Night Talking", "Harry Styles", "Harry's House"),
("Die For You", "The Weeknd", "Starboy"),
("Blinding Lights", "The Weeknd", "After Hours"),
("Starboy", "The Weeknd", "Starboy"),
("Shape of You", "Ed Sheeran", "Divide"),
("Bad Guy", "Billie Eilish", "When We All Fall Asleep, Where Do We Go?"),
("Flowers", "Miley Cyrus", "Endless Summer Vacation"),
("Stay", "The Kid LAROI & Justin Bieber", "F*ck Love 3: Over You"),
("七里香", "周杰伦", "七里香"),
("晴天", "周杰伦", "叶惠美"),
("一路向北", "周杰伦", "11月的肖邦"),
("告白气球", "周杰伦", "周杰伦的床边故事"),
("十年", "陈奕迅", "黑·白·灰"),
("富士山下", "陈奕迅", "What's Going On...?"),
("孤勇者", "陈奕迅", "孤勇者"),
("修炼爱情", "林俊杰", "因你而在"),
("江南", "林俊杰", "第二天堂"),
("光年之外", "G.E.M. 邓紫棋", "摩天动物园"),
("泡沫", "G.E.M. 邓紫棋", "Xposed"),
("因为爱情", "王菲 & 陈奕迅", "Stranger Under My Skin"),
("红豆", "王菲", "唱游"),
("Bohemian Rhapsody", "Queen", "A Night at the Opera"),
("Don't Stop Me Now", "Queen", "Jazz"),
("Numb", "Linkin Park", "Meteora"),
("In the End", "Linkin Park", "Hybrid Theory"),
("Yellow", "Coldplay", "Parachutes"),
("Viva La Vida", "Coldplay", "Viva La Vida"),
("Smells Like Teen Spirit", "Nirvana", "Nevermind"),
("Hotel California", "Eagles", "Hotel California"),
("Lemon", "米津玄師", "Lemon"),
("Kick Back", "米津玄師", "KICK BACK"),
("アイドル", "YOASOBI", "アイドル"),
("夜に駆ける", "YOASOBI", "THE BOOK"),
("First Love", "宇多田ヒカル", "First Love"),
("Dynamite", "BTS", "BE"),
("Butter", "BTS", "Butter"),
("How You Like That", "BLACKPINK", "The Album"),
("Ditto", "NewJeans", "OMG"),
("Get Lucky", "Daft Punk", "Random Access Memories"),
("The Nights", "Avicii", "The Days / Nights"),
("Summer", "Calvin Harris", "Motion"),
};
var playerIds = new[] { "Spotify", "Spotify", "Spotify", "MusicBee", "MusicBee", "QQMusic", "NeteaseCloudMusic", "AppleMusic" };
var batchList = new List<PlayHistoryItem>();
// 我们尝试生成 count 条有效数据
// 为了防止死循环,加个硬上限
int attempts = 0;
while (batchList.Count < count && attempts < count * 5)
{
attempts++;
var song = presetSongs[random.Next(presetSongs.Count)];
var playerId = playerIds[random.Next(playerIds.Length)];
var daysBack = random.Next(0, 365);
var hoursBack = random.Next(0, 24);
var minutesBack = random.Next(0, 60);
var secondsBack = random.Next(0, 60);
var startedAt = DateTime.UtcNow // 直接用 UTC
.AddDays(-daysBack)
.AddHours(-hoursBack)
.AddMinutes(-minutesBack)
.AddSeconds(-secondsBack);
var totalDurationMs = random.Next(180, 300) * 1000.0;
double playedRatio;
double roll = random.NextDouble();
if (roll > 0.3) playedRatio = 0.9 + (random.NextDouble() * 0.1);
else if (roll > 0.1) playedRatio = 0.3 + (random.NextDouble() * 0.5);
else playedRatio = 0.05 + (random.NextDouble() * 0.25);
var playedDurationMs = totalDurationMs * playedRatio;
// 只有听了一半以上的才算作记录
if (playedDurationMs >= (totalDurationMs / 2))
{
batchList.Add(new PlayHistoryItem
{
Title = song.Title,
Artist = song.Artist,
Album = song.Album,
PlayerId = playerId,
StartedAt = startedAt,
TotalDurationMs = totalDurationMs,
DurationPlayedMs = playedDurationMs
});
}
}
if (batchList.Count > 0)
{
using var context = await _contextFactory.CreateDbContextAsync();
await context.PlayHistory.AddRangeAsync(batchList);
await context.SaveChangesAsync();
}
}
}
}

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>الوضع المثبت (Docked)</value>
</data>
<data name="Error" xml:space="preserve">
<value>خطأ</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>تم التصدير بنجاح</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>التحضير لتنظيف ذاكرة التخزين المؤقت...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>جاهز</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>تم اكتشاف مسار الدليل الجذر. قد يحتوي فهرس القرص الكامل على عدد كبير من الملفات غير الوسائط ويتسبب في استغراق الفحص وقتاً طويلاً جداً. يوصى بتحديد دليل فرعي محدد.</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>كل الموسيقى</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>مزامنة مكتبة الوسائط جارية...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>توجد مشكلة في مزامنة مكتبة الوسائط</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>مسح قائمة الانتظار</value>
</data>
@@ -691,7 +703,7 @@
<value>كمية الضبابية (Blur)</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>يشمل ملفات السجل وذاكرة التخزين المؤقت للكلمات عبر الشبكة</value>
<value>يتضمن ملفات السجلات، وذاكرة التخزين المؤقت للكلمات عبر الإنترنت</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>ذاكرة التخزين المؤقت (Cache)</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>الخروج من البرنامج عند إغلاق نافذة الكلمات</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>تصدير تاريخ اللعب</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>تصدير الإعدادات</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>مدير الإعدادات</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>تاريخ اللعب</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>تصفح مركز مشاركة الموارد عبر الإنترنت</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>بدء التشغيل</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>الإحصائيات</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>إيقاف التشغيل عند إغلاق نافذة مكتبة الموسيقى</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>الوضع القياسي</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>المصادر</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>هذا الشهر</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>هذا الربع</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>هذا الأسبوع</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>هذا العام</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>اليوم</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>كبار الفنانين</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>أعلى المسارات</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>المصدر الأعلى</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>المدة الإجمالية</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>المسارات التي تم تشغيلها</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>خروج</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>Angedockter Modus</value>
</data>
<data name="Error" xml:space="preserve">
<value>Fehler</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Export erfolgreich</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Bereinigung des Caches vorbereiten...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Bereit</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>Der Pfad zum Stammverzeichnis wurde erkannt. Ein vollständiger Festplattenindex kann eine große Anzahl von Nicht-Mediendateien enthalten und dazu führen, dass die Suche zu lange dauert. Es wird empfohlen, ein bestimmtes Unterverzeichnis anzugeben.</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Alle Musikstücke</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Synchronisierung der Medienbibliothek läuft...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Es gibt ein Problem mit der Synchronisierung der Medienbibliothek</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Warteschlange leeren</value>
</data>
@@ -691,7 +703,7 @@
<value>Unschärfe-Stärke</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Einschließlich Protokolldateien, Online-Songtext-Cache</value>
<value>Enthält Protokolldateien, Online-Liedtext-Cache</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>App beenden, wenn Songtext-Fenster geschlossen wird</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Spielverlauf exportieren</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Einstellungen exportieren</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Einstellungsmanager</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Geschichte spielen</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Online Share Hub durchsuchen</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Start</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Statistik</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Wiedergabe stoppen, wenn Musikgalerie-Fenster geschlossen wird</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>Standard-Modus</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Quellen</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>Dieser Monat</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>Dieses Quartal</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>Diese Woche</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>Dieses Jahr</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>Heute</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Top Künstler</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Top Tracks</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Oberste Quelle</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Gesamtdauer</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Gespielte Tracks</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Beenden</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>Docked Mode</value>
</data>
<data name="Error" xml:space="preserve">
<value>Error</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Export successful</value>
</data>
@@ -166,7 +169,7 @@
<value>Unable to connect to LX Music Server. Please go to Settings - Playback Source - LX Music - LX Music Server to check if the link is entered correctly</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>Cleaning cache...</value>
<value>Clearing cache...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>Connection failed</value>
@@ -181,7 +184,10 @@
<value>Parsing...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Preparing to clean cache...</value>
<value>Preparing to clear cache...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Ready</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>The root directory path has been detected. A full disk index may contain a large number of non-media files and cause the scan to take too long. It is recommended to specify a specific subdirectory.</value>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>All Music</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Media Library sync in progress...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>There is a problem with Media Library sync</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Clear queue</value>
</data>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Exit app when lyrics window is closed</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Export play history</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Export Settings</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Settings Manager</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Play History</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Browse Online Share Hub</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Startup</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Statistics</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Stop playback when Music Gallery window is closed</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>Standard Mode</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sources</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>This Month</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>This Quarter</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>This Week</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>This Year</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>Today</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Top Artists</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Top Tracks</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Top Source</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Total Duration</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Tracks Played</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Exit</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>Modo Acoplado</value>
</data>
<data name="Error" xml:space="preserve">
<value>Error</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Exportación exitosa</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Preparando la limpieza de caché...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Listo</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>Se ha detectado la ruta del directorio raíz. Un índice de disco completo puede contener un gran número de archivos no multimedia y hacer que la exploración dure demasiado tiempo. Se recomienda especificar un subdirectorio concreto.</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Toda la música</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Sincronización de la biblioteca multimedia en curso...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Hay un problema con la sincronización de la biblioteca multimedia</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Borrar cola</value>
</data>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Salir de la app al cerrar la ventana de letras</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Exportar historial de jugadas</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Exportar configuración</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Gestor de configuración</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Historia del juego</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Explorar Centro de recursos en línea</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Inicio</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Estadísticas</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Detener reproducción al cerrar ventana de Galería de Música</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>Modo Estándar</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Fuentes</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>Este mes</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>Este trimestre</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>Esta semana</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>Este año</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>Hoy</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Artistas principales</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Top Tracks</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Fuente superior</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Duración total</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Pistas reproducidas</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Salir</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>Mode Ancré</value>
</data>
<data name="Error" xml:space="preserve">
<value>Erreur</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Exportation réussie</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Préparation du nettoyage du cache...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Prêt</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>Le chemin du répertoire racine a été détecté. Un index de disque complet peut contenir un grand nombre de fichiers non multimédias et faire durer l'analyse trop longtemps. Il est recommandé de spécifier un sous-répertoire spécifique.</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Toute la musique</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Synchronisation de la médiathèque en cours...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Il y a un problème avec la synchronisation de la médiathèque</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Vider la file d'attente</value>
</data>
@@ -691,7 +703,7 @@
<value>Quantité de flou</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Comprend les fichiers journaux, le cache des paroles en ligne</value>
<value>Inclut les fichiers journaux, le cache des paroles en ligne</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Quitter l'app à la fermeture de la fenêtre des paroles</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Exporter l'historique des jeux</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Exporter les paramètres</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Gestionnaire de paramètres</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Histoire du jeu</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Parcourir le Hub de partage en ligne</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Démarrage</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Statistiques</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Arrêter la lecture à la fermeture de la Galerie</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>Mode Standard</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sources d'information</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>Ce mois-ci</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>Ce trimestre</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>Cette semaine</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>Cette année</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>Aujourd'hui</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Artistes de haut niveau</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Top Tracks</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Source supérieure</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Durée totale</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Pistes jouées</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Quitter</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>डॉक्ड मोड</value>
</data>
<data name="Error" xml:space="preserve">
<value>त्रुटि</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>निर्यात सफल रहा</value>
</data>
@@ -166,7 +169,7 @@
<value>LX Music सर्वर से कनेक्ट नहीं हो सकता, कृपया सेटिंग्स - प्लेबैक स्रोत - LX Music - LX Music सर्वर पर जाएं और जांचें कि लिंक सही तरीके से दर्ज किया गया है या नहीं</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>Cleaning cache...</value>
<value>कैश साफ़ हो रहा है...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>कनेक्शन विफल रहा</value>
@@ -175,19 +178,22 @@
<value>कनेक्टिंग ...</value>
</data>
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
<value>Fetching file list...</value>
<value>फ़ाइल लिस्ट लाई जा रही है...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>Parsing...</value>
<value>पार्सिंग...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Preparing to clean cache...</value>
<value>कैश साफ़ करने की तैयारी...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>तैयार</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>The root directory path has been detected. A full disk index may contain a large number of non-media files and cause the scan to take too long. It is recommended to specify a specific subdirectory.</value>
<value>रूट डायरेक्टरी पाथ का पता चल गया है। एक फुल डिस्क इंडेक्स में बड़ी संख्या में नॉन-मीडिया फ़ाइलें हो सकती हैं और स्कैन में बहुत ज़्यादा समय लग सकता है। एक खास सबडायरेक्टरी बताने की सलाह दी जाती है।</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>Preparing to scan...</value>
<value>स्कैन करने की तैयारी...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>फुलस्क्रीन मोड</value>
@@ -382,13 +388,13 @@
<value>विभाजित दृश्य</value>
</data>
<data name="MediaSettingsControlLastSyncTime.Header" xml:space="preserve">
<value>Last Sync Time</value>
<value>अंतिम सिंक समय</value>
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value>Local Folder</value>
<value>स्थानीय फोल्डर</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>Name</value>
<value>नाम</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>अभी सिंक्रनाइज़ करें</value>
@@ -403,11 +409,17 @@
<value>अगला आइटम</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Text" xml:space="preserve">
<value>Add to playing queue</value>
<value>प्लेइंग क्यू में जोड़ें</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>सभी गाने</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>मीडिया लाइब्रेरी सिंक चल रहा है...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>मीडिया लाइब्रेरी सिंक में कोई समस्या है</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>प्लेइंग कतार साफ़ करें</value>
</data>
@@ -451,13 +463,13 @@
<value>मीडिया लाइब्रेरी में कोई गाना नहीं मिला</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>Folders</value>
<value>फ़ोल्डर्स</value>
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>फ़ाइल से आयात करें</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>Create playlist</value>
<value>प्लेलिस्ट बनायें</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>प्लेइंग कतार</value>
@@ -466,7 +478,7 @@
<value>प्लेइंग कतार खाली है</value>
</data>
<data name="MusicGalleryPagePlaylist.Text" xml:space="preserve">
<value>Playlists</value>
<value>प्लेलिस्ट</value>
</data>
<data name="MusicGalleryPageQueueLoop.Text" xml:space="preserve">
<value>सूची लूप</value>
@@ -511,7 +523,7 @@
<value>संगीत लाइब्रेरी - BetterLyrics</value>
</data>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>Auto-sync Frequency</value>
<value>ऑटो-सिंक आवृत्ति</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>कभी नहीं</value>
@@ -544,13 +556,13 @@
<value>गोपनीयता नीति</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>Browse</value>
<value>ब्राउज करें</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>Name</value>
<value>नाम</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>Leaving it blank will automatically generate a default name.</value>
<value>इसे खाली छोड़ने से अपने आप एक डिफ़ॉल्ट नाम बन जाएगा।</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>पासवर्ड</value>
@@ -559,10 +571,10 @@
<value>पथ</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>The specified folder path could not be found</value>
<value>बताया गया फ़ोल्डर पाथ नहीं मिला</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>Path is required</value>
<value>पथ आवश्यक है</value>
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>पोर्ट</value>
@@ -691,7 +703,7 @@
<value>धुंधलापन मात्रा</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>लॉग फ़ाइलें, नेटवर्क बोल कैश शामिल हैं</value>
<value>लॉग फ़ाइलें, ऑनलाइन लिरिक्स कैश शामिल हैं</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>कैश</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>बोल विंडो बंद होने पर बाहर निकलें</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>प्ले इतिहास निर्यात करें</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>सेटिंग्स निर्यात करें</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>सेटिंग्स प्रबंधक</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>प्ले इतिहास</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>ऑनलाइन संसाधन शेयरिंग हब ब्राउज़ करें</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>स्टार्टअप</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>आंकड़े</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>संगीत गैलरी विंडो बंद होने पर प्लेबैक रोकें</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>मानक मोड</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>स्रोत</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>इस महीने</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>इस तिमाही</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>इस सप्ताह</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>इस वर्ष</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>आज</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>शीर्ष कलाकार</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>शीर्ष रास्ता</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>शीर्ष स्रोत</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>कुल अवधि</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>बजाए गए ट्रैक</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>कार्यक्रम से बाहर निकलें</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>Mode Dok</value>
</data>
<data name="Error" xml:space="preserve">
<value>Kesalahan</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Ekspor berhasil</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Bersiap untuk membersihkan cache...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Siap</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>Jalur direktori root telah terdeteksi. Indeks disk penuh mungkin berisi sejumlah besar file non-media dan menyebabkan pemindaian memakan waktu terlalu lama. Disarankan untuk menentukan subdirektori tertentu.</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Semua Musik</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Sinkronisasi Perpustakaan Media sedang berlangsung...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Ada masalah dengan sinkronisasi Perpustakaan Media</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Bersihkan antrean putar</value>
</data>
@@ -691,7 +703,7 @@
<value>Tingkat Keburaman</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Termasuk file log, cache lirik jaringan</value>
<value>Termasuk file log, cache lirik online</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Keluar dari program saat jendela lirik ditutup</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Riwayat permainan ekspor</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Ekspor Pengaturan</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Manajer Pengaturan</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Riwayat Bermain</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Jelajahi Hub Berbagi Sumber Daya Online</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Startup</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Statistik</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Hentikan pemutaran saat jendela galeri musik ditutup</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>Mode Standar</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sumber</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>Bulan ini</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>Kuartal ini</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>Minggu Ini</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>Tahun ini</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>Hari ini</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Artis Top</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Trek Teratas</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Sumber Teratas</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Total Durasi</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Lagu yang Dimainkan</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Keluar dari Program</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>ドッキングモード</value>
</data>
<data name="Error" xml:space="preserve">
<value>エラー</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>エクスポートが成功しました</value>
</data>
@@ -166,7 +169,7 @@
<value>LX Music サーバーに接続できません。「設定」-「再生ソース」-「LX Music」-「LX Music サーバー」に移動し、リンクが正しく入力されているか確認してください</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>キャッシュをクリーニング中...</value>
<value>キャッシュをクリ中...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>接続に失敗しました</value>
@@ -178,16 +181,19 @@
<value>ファイルリストを取得しています...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>Parsing...</value>
<value>解析中...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>キャッシュクリーン準備中</value>
<value>キャッシュクリアを準備中...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>準備完了</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>ルートディレクトリのパスが検出された。フルディスクインデックスにはメディア以外のファイルが多数含まれている可能性があり、スキャンに時間がかかりすぎる。特定のサブディレクトリを指定することを推奨する。</value>
<value>ルートディレクトリが指定されました。フルディスクインデックス作成には大量の非メディアファイルが含まれる可能性があり、スキャンに時間がかかる恐れがあります。特定のサブディレクトリを指定することをお勧めします。</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>スキャンを待っています...</value>
<value>スキャン準備中...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>全画面モード</value>
@@ -253,13 +259,13 @@
<value>歌詞の言語</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>歌詞提供</value>
<value>歌詞提供</value>
</data>
<data name="LyricsPageLyricsSearch.Text" xml:space="preserve">
<value>歌詞検索</value>
</data>
<data name="LyricsPageLyricsSettings.Text" xml:space="preserve">
<value>歌詞ウィンドウのショートカット</value>
<value>歌詞ウィンドウのショートカット設定</value>
</data>
<data name="LyricsPageMatchPercentage.Header" xml:space="preserve">
<value>一致率</value>
@@ -283,7 +289,7 @@
<value>翻訳提供元</value>
</data>
<data name="LyricsPageTransliterationProviderPrefix.Header" xml:space="preserve">
<value>翻訳提供元</value>
<value>ルビ提供元</value>
</data>
<data name="LyricsParseError" xml:space="preserve">
<value>歌詞の解析に失敗しました</value>
@@ -301,7 +307,7 @@
<value>曲の長さ</value>
</data>
<data name="LyricsSearchControlHelp.Text" xml:space="preserve">
<value>* 変更は保存後すぐに反映され、マッピング情報とターゲットの歌詞を使用して後続の曲の歌詞が取得されます。 「Instrumental」としてマークすると、「Instrumental」プレースホルダ歌詞に直接戻ります。 リセットすると元のデータの検索が復元されます。</value>
<value>* 変更は保存後すぐに反映され、以降はマッピング情報に基づいた歌詞取得が行われます。「インストゥルメンタル」としてマークすると、専用のプレースホルダ歌詞に切り替わります。リセットを実行すると元のデータ取得状態に戻ります。</value>
</data>
<data name="LyricsSearchControlIgnoreCache.Header" xml:space="preserve">
<value>検索時にキャッシュを無視する</value>
@@ -310,7 +316,7 @@
<value>マッピング先</value>
</data>
<data name="LyricsSearchControlMarkAsPureMusic.Content" xml:space="preserve">
<value>インストとしてマーク</value>
<value>インストゥルメンタルとしてマーク</value>
</data>
<data name="LyricsSearchControlNotFound.Text" xml:space="preserve">
<value>見つかりません</value>
@@ -361,10 +367,10 @@
<value>さらにモードを追加するには「設定」に移動します</value>
</data>
<data name="LyricsWindowSwitchWindowTitle" xml:space="preserve">
<value>歌詞ウィンドウ切替</value>
<value>歌詞ウィンドウスイッチャー</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>アルバムアートエリアのみ表示</value>
<value>アルバムアートのみ表示</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>歌詞のみ表示</value>
@@ -388,7 +394,7 @@
<value>ローカルフォルダー</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>名</value>
<value>名</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>今すぐ同期</value>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>すべてのミュージック</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>メディアライブラリの同期中...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>メディアライブラリの同期に問題が発生しました</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>キューをクリア</value>
</data>
@@ -511,22 +523,22 @@
<value>ミュージックギャラリー - BetterLyrics</value>
</data>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>自動同期周波数</value>
<value>自動同期の頻度</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>一切なし</value>
<value>なし</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
<value>毎日</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
<value>15分ごと</value>
<value>15分間隔</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
<value>1時間ごと</value>
<value>1時間間隔</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
<value>6時間ごと</value>
<value>6時間間隔</value>
</data>
<data name="NarrowMode" xml:space="preserve">
<value>狭い表示モード</value>
@@ -544,13 +556,13 @@
<value>個人情報保護方針</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>ブラウズ</value>
<value>参照</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>名称</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>空のままにしておくと、自動的にデフォルトの名前が生成され。</value>
<value>空のままにすると、デフォルトの名前が自動生成されます。</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>パスワード</value>
@@ -559,7 +571,7 @@
<value>パス</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>指定されたフォルダのパスが見つかりません</value>
<value>指定されたパスが見つかりません</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>パスが必要</value>
@@ -601,7 +613,7 @@
<value>デザイン参考</value>
</data>
<data name="SettingsPage3DLyrics.Header" xml:space="preserve">
<value>[実験的] 深度エフェクト</value>
<value>[実験的] 奥行きのエフェクト</value>
</data>
<data name="SettingsPage3DLyricsDepth.Header" xml:space="preserve">
<value>深度</value>
@@ -676,10 +688,10 @@
<value>起動時にデフォルトの歌詞ウィンドウを自動的に開く</value>
</data>
<data name="SettingsPageAutoOpenMusicGalleryWindow.Header" xml:space="preserve">
<value>起動時にギャラリーウィンドウを開く</value>
<value>起動時にミュージックギャラリーウィンドウを開く</value>
</data>
<data name="SettingsPageAutoPlayWhenOpenMusicGalleryWindow.Header" xml:space="preserve">
<value>ギャラリーウィンドウを開いたときに自動的に再生を再開</value>
<value>ミュージックギャラリーを開いたときに自動的に再生を再開する</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自動起動</value>
@@ -700,7 +712,7 @@
<value>中央揃え</value>
</data>
<data name="SettingsPageCheckShortcut.Content" xml:space="preserve">
<value>キー割り当ての確認</value>
<value>ショートカットキーの確認</value>
</data>
<data name="SettingsPageChinese.Header" xml:space="preserve">
<value>ピンインルビ</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>歌詞ウィンドウを閉じたときにアプリを終了する</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>再生履歴をエクスポート</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>設定をエクスポート</value>
</data>
@@ -871,7 +886,7 @@
<value>定期チェックで最前面表示を強制維持します</value>
</data>
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>常に上部に強制的に表示</value>
<value>常にトップに強制的に表示</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>現在の歌詞に文字単位の情報がない場合でも、文字単位の歌詞シミュレーションをします</value>
@@ -1006,7 +1021,7 @@
<value>現在の行の位置</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌詞エフェクト</value>
<value>歌詞エフェクト</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>超極太</value>
@@ -1093,7 +1108,7 @@
<value>順次:以下のリストの優先順位に従って検索し、最初に見つかった結果を返します。ベストマッチ:有効なすべてのソースを検索し、一致スコアが最も高い結果を自動的に選択します</value>
</data>
<data name="SettingsPageLyricsSearchType.Header" xml:space="preserve">
<value>歌詞検索方法</value>
<value>歌詞検索方法</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>中太</value>
@@ -1102,7 +1117,7 @@
<value>準細</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌詞スタイル</value>
<value>歌詞スタイル</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>極細</value>
@@ -1123,13 +1138,13 @@
<value>歌詞ウィンドウマネージャー</value>
</data>
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>歌詞ウィンドウ状態切り替えショートカット</value>
<value>歌詞ウィンドウスイッチャーのショートカットキー</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>この値を調整すると、順次検索とベストマッチ検索の結果に影響しますが、手動歌詞検索インターフェイスの検索結果には影響しません</value>
</data>
<data name="SettingsPageMatchingThreshold.Header" xml:space="preserve">
<value>一致度の最低ライン</value>
<value>最小一致しきい値</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>メディアライブラリ</value>
@@ -1201,7 +1216,7 @@
<value>歌詞のルビ</value>
</data>
<data name="SettingsPagePhoneticText.Header" xml:space="preserve">
<value>ルビのサイズ</value>
<value>ルビ</value>
</data>
<data name="SettingsPagePinToTaskbar.Header" xml:space="preserve">
<value>タスクバーにピン留め</value>
@@ -1228,10 +1243,10 @@
<value>"soundcloud.com" で "Cut to the Feeling" を再生</value>
</data>
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>再生/一時停止のショートカット</value>
<value>再生/一時停止のショートカットキー</value>
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>前の曲へのショートカット</value>
<value>前の曲へのショートカットキー</value>
</data>
<data name="SettingsPagePureLayer.Header" xml:space="preserve">
<value>単色のレイヤー</value>
@@ -1299,11 +1314,14 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>設定マネージャー</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>再生履歴</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>オンライン共有センターを閲覧する</value>
</data>
<data name="SettingsPageShortcut.Text" xml:space="preserve">
<value>ショートカット</value>
<value>ショートカットキー</value>
</data>
<data name="SettingsPageShortcutRegFailInfo" xml:space="preserve">
<value>ホットキーの登録に失敗しました</value>
@@ -1318,7 +1336,7 @@
<value>アーティストを表示</value>
</data>
<data name="SettingsPageShowHideHotKey.Header" xml:space="preserve">
<value>歌詞ウィンドウの表示/非表示ショートカット</value>
<value>歌詞ウィンドウの表示/非表示ショートカットキー</value>
</data>
<data name="SettingsPageShowInSwitchers.Description" xml:space="preserve">
<value>例: Alt + Tab、タスクバー</value>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>スタートアップ</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>統計データ</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>ミュージックギャラリーウィンドウを閉じたときに再生を停止する</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>標準モード</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>再生ソース</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>今月</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>今期</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>今週</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>今年</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>今日</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>トップアーティスト</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>トップトラック</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>よく使う再生ソース</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>総再生時間</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>再生された曲の数</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>終了</value>
</data>
@@ -1468,7 +1522,7 @@
<value>設定を開く</value>
</data>
<data name="SystemTraySwitch.Text" xml:space="preserve">
<value>歌詞ウィンドウ切り替え</value>
<value>歌詞ウィンドウスイッチャー</value>
</data>
<data name="TaskbarMode" xml:space="preserve">
<value>タスクバー モード</value>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>도킹 모드</value>
</data>
<data name="Error" xml:space="preserve">
<value>오류</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>내보내기 성공</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>캐시 정리 준비 중...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>준비</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>루트 디렉터리 경로가 감지되었습니다. 전체 디스크 인덱스에 미디어가 아닌 파일이 많이 포함되어 있어 스캔 시간이 너무 오래 걸릴 수 있습니다. 특정 하위 디렉터리를 지정하는 것이 좋습니다.</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>모든 음악</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>미디어 라이브러리 동기화 진행 중...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>미디어 라이브러리 동기화에 문제가 있습니다.</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>재생 대기열 비우기</value>
</data>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>가사 창을 닫을 때 앱 종료</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>플레이 기록 내보내기</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>설정 내보내기</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>설정 관리자</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>플레이 기록</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>온라인 공유 허브 탐색</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>시작</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>통계</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>음악 갤러리 창을 닫을 때 재생 중지</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>표준 모드</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>출처</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>이번 달</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>이번 분기</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>이번 주</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>올해</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>오늘</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>톱 아티스트</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>인기 트랙</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>상위 소스</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>총 기간</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>재생된 트랙</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>종료</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>Mod Dok</value>
</data>
<data name="Error" xml:space="preserve">
<value>Ralat</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Eksport berjaya</value>
</data>
@@ -166,7 +169,7 @@
<value>Tidak dapat menyambung ke pelayan LX Music, sila pergi ke Tetapan - Sumber Main Balik - LX Music - Pelayan LX Music untuk menyemak sama ada pautan dimasukkan dengan betul</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>Cleaning cache...</value>
<value>Membersihkan cache...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>Sambungan gagal</value>
@@ -175,19 +178,22 @@
<value>Menyambung...</value>
</data>
<data name="FileSystemServiceFetchingFileList" xml:space="preserve">
<value>Fetching file list...</value>
<value>Mengambil senarai fail...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>Menghurai...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Preparing to clean cache...</value>
<value>Bersedia untuk membersihkan cache...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Sedia</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>The root directory path has been detected. A full disk index may contain a large number of non-media files and cause the scan to take too long. It is recommended to specify a specific subdirectory.</value>
<value>Laluan direktori akar telah dikesan. Indeks cakera penuh mungkin mengandungi sejumlah besar fail bukan media dan menyebabkan imbasan mengambil masa terlalu lama. Adalah disyorkan untuk menentukan subdirektori tertentu.</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>Preparing to scan...</value>
<value>Bersedia untuk mengimbas...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>Mod Skrin Penuh</value>
@@ -385,10 +391,10 @@
<value>Masa Segerak Akhir</value>
</data>
<data name="MediaSettingsControlLocalFolder" xml:space="preserve">
<value>Local Folder</value>
<value>Folder setempat</value>
</data>
<data name="MediaSettingsControlNameSetting.Header" xml:space="preserve">
<value>Name</value>
<value>Nama</value>
</data>
<data name="MediaSettingsControlSyncNow.Content" xml:space="preserve">
<value>Segerak sekarang</value>
@@ -403,11 +409,17 @@
<value>Item seterusnya</value>
</data>
<data name="MusicGalleryPageAddToPlayingQueue.Text" xml:space="preserve">
<value>Add to playing queue</value>
<value>Tambah ke turutan lagu</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Semua Muzik</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Penyegerakan Pustaka Media sedang dijalankan...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Terdapat masalah dengan penyegerakan Pustaka Media</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Kosongkan baris gilir main</value>
</data>
@@ -451,13 +463,13 @@
<value>Tiada lagu ditemui dalam pustaka media</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>Folders</value>
<value>Folder</value>
</data>
<data name="MusicGalleryPageImportFromFile.Text" xml:space="preserve">
<value>Import dari fail</value>
</data>
<data name="MusicGalleryPageNewPlaylist.Text" xml:space="preserve">
<value>Create playlist</value>
<value>Cipta senarai main</value>
</data>
<data name="MusicGalleryPagePlayingQueue.Text" xml:space="preserve">
<value>Baris Gilir Main</value>
@@ -466,7 +478,7 @@
<value>Baris gilir main kosong</value>
</data>
<data name="MusicGalleryPagePlaylist.Text" xml:space="preserve">
<value>Playlists</value>
<value>Senarai Main</value>
</data>
<data name="MusicGalleryPageQueueLoop.Text" xml:space="preserve">
<value>Gelung senarai</value>
@@ -511,7 +523,7 @@
<value>Galeri Muzik - BetterLyrics</value>
</data>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>Auto-sync Frequency</value>
<value>Kekerapan penyegerakan automatik</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>Tak pernah</value>
@@ -526,7 +538,7 @@
<value>Setiap jam</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
<value>Every 6 Hours</value>
<value>Setiap 6 Jam</value>
</data>
<data name="NarrowMode" xml:space="preserve">
<value>Mod Sempit</value>
@@ -544,13 +556,13 @@
<value>Dasar Privasi</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>Browse</value>
<value>Layari</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>Name</value>
<value>Nama</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>Leaving it blank will automatically generate a default name.</value>
<value>Membiarkan ia kosong akan menjana nama lalai secara automatik.</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>Kata Laluan</value>
@@ -559,10 +571,10 @@
<value>Laluan</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>The specified folder path could not be found</value>
<value>Laluan folder yang dinyatakan tidak dapat ditemui</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>Path is required</value>
<value>Laluan diperlukan</value>
</data>
<data name="RemoteServerConfigControlPort.Header" xml:space="preserve">
<value>Port</value>
@@ -691,7 +703,7 @@
<value>Jumlah Kabur</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Termasuk fail log, cache lirik rangkaian</value>
<value>Termasuk fail log, cache lirik dalam talian</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Keluar dari program apabila tetingkap lirik ditutup</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Eksport sejarah main</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Eksport Tetapan</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Pengurus Tetapan</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Sejarah Main</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Layari Hab Perkongsian Sumber Dalam Talian</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Permulaan</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Statistik</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Hentikan main balik apabila tetingkap galeri muzik ditutup</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>Mod Standard</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sumber</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>Bulan Ini</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>Suku Tahun Ini</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>Minggu Ini</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>Tahun Ini</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>Hari ini</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Artis Teratas</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Lagu top</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Sumber Utama</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Jumlah Tempoh</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Trek Dimainkan</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Keluar Program</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>Modo Acoplado</value>
</data>
<data name="Error" xml:space="preserve">
<value>Erro</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Exportação bem-sucedida</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>A preparar a limpeza da cache...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Pronto</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>O caminho do diretório raiz foi detectado. Um índice de disco completo pode conter um grande número de ficheiros não multimédia e fazer com que a pesquisa demore demasiado tempo. Recomenda-se a especificação de um subdiretório específico.</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Todas as Músicas</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Sincronização da biblioteca multimédia em curso...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Existe um problema com a sincronização da Biblioteca Multimédia</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Limpar fila de reprodução</value>
</data>
@@ -691,7 +703,7 @@
<value>Quantidade de Desfoque</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Inclui ficheiros de registo e cache de letras da rede</value>
<value>Inclui ficheiros de registo, cache de letras online</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Cache</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Sair do programa ao fechar a janela de letras</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Exportar histórico de jogos</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Exportar Definições</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Gestor de Definições</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>História do jogo</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Explorar o Centro de Partilha de Recursos Online</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Arranque</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Estatísticas</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Parar reprodução ao fechar a janela da galeria de música</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>Modo Padrão</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Fontes</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>Este mês</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>Este trimestre</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>Esta semana</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>Este ano</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>Hoje</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Artistas de topo</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Faixas superiores</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Fonte superior</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Duração total</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Faixas reproduzidas</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Sair do Programa</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>Закрепленный режим</value>
</data>
<data name="Error" xml:space="preserve">
<value>Ошибка</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Экспорт выполнен успешно</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Подготовка к очистке кэша...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Готовые</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>Обнаружен путь к корневому каталогу. Полный индекс диска может содержать большое количество файлов, не относящихся к мультимедиа, что приведет к слишком долгому сканированию. Рекомендуется указывать конкретный подкаталог.</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Вся музыка</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Выполняется синхронизация медиатеки...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Возникла проблема с синхронизацией медиатеки</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Очистить очередь воспроизведения</value>
</data>
@@ -691,7 +703,7 @@
<value>Степень размытия</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Включает лог-файлы, кэш онлайн-текстов</value>
<value>Включая файлы журналов, кэш онлайновых текстов песен</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Кэш</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Выходить из программы при закрытии окна текста</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Экспорт истории игр</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Экспорт настроек</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Менеджер настроек</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>История игры</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Просмотреть онлайн-центр обмена</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Запуск</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Статистика</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Останавливать воспроизведение при закрытии Галереи</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>Стандартный режим</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Источники</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>Этот месяц</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>Этот квартал</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>На этой неделе</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>В этом году</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>Сегодня</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Лучшие художники</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Лучшие треки</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Верхний источник</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Общая продолжительность</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Воспроизведенные треки</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Выход</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>โหมดเชื่อมต่อ (Docked)</value>
</data>
<data name="Error" xml:space="preserve">
<value>ข้อผิดพลาด</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>ส่งออกสำเร็จ</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>กำลังเตรียมล้างแคช...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>พร้อม</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>พบเส้นทางไดเรกทอรีรากแล้ว ดัชนีของดิสก์ทั้งหมดอาจมีไฟล์ที่ไม่ใช่สื่อจำนวนมากและทำให้การสแกนใช้เวลานานเกินไป ขอแนะนำให้ระบุไดเรกทอรีย่อยเฉพาะ</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>เพลงทั้งหมด</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>กำลังซิงค์คลังสื่อ...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>มีปัญหาเกี่ยวกับการซิงค์ไลบรารีมีเดีย</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>ล้างคิวการเล่น</value>
</data>
@@ -691,7 +703,7 @@
<value>ปริมาณความเบลอ</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>รวมไฟล์บันทึก, แคชเนื้อเพลงจากเครือข่าย</value>
<value>รวมถึงไฟล์บันทึก, แคชเนื้อเพลงออนไลน์</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>แคช</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>ออกจากโปรแกรมเมื่อปิดหน้าต่างเนื้อเพลง</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>ส่งออกประวัติการเล่น</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>ส่งออกการตั้งค่า</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>ตัวจัดการการตั้งค่า</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>ประวัติการเล่น</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>เรียกดูศูนย์แบ่งปันทรัพยากรออนไลน์</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>การเริ่มระบบ</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>สถิติ</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>หยุดเล่นเมื่อปิดหน้าต่างคลังเพลง</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>โหมดมาตรฐาน</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>แหล่งข้อมูล</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>เดือนนี้</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>ไตรมาสนี้</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>สัปดาห์นี้</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>ปีนี้</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>วันนี้</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>ศิลปินชั้นนำ</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>เพลงยอดนิยม</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>แหล่งข้อมูลหลัก</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>ระยะเวลาทั้งหมด</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>เพลงที่เล่น</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>ออกจากโปรแกรม</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>Chế độ neo (Docked)</value>
</data>
<data name="Error" xml:space="preserve">
<value>Lỗi</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>Xuất thành công</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>Đang chuẩn bị xóa bộ nhớ cache...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>Sẵn sàng</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>Đường dẫn thư mục gốc đã được phát hiện. Chỉ mục đĩa đầy đủ có thể chứa một lượng lớn tệp không phải phương tiện và khiến quá trình quét mất quá nhiều thời gian. Khuyến nghị chỉ định một thư mục con cụ thể.</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>Tất cả bài hát</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>Đang đồng bộ thư viện phương tiện...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>Có vấn đề với việc đồng bộ hóa Thư viện Phương tiện.</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>Xóa hàng đợi phát</value>
</data>
@@ -691,7 +703,7 @@
<value>Độ mờ</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>Bao gồm tệp nhật ký, bộ nhớ đệm lời bài hát mạng</value>
<value>Gồm các tệp nhật ký bộ nhớ đệm lời bài hát trực tuyến.</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>Bộ nhớ đệm</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>Thoát chương trình khi đóng cửa sổ lời bài hát</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>Xuất lịch sử phát lại</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>Xuất cài đặt</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>Trình quản lý cài đặt</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>Lịch sử chơi</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>Duyệt trung tâm chia sẻ tài nguyên trực tuyến</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>Khởi động</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>Thống kê</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>Dừng phát khi đóng cửa sổ thư viện nhạc</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>Chế độ tiêu chuẩn</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Nguồn</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>Tháng này</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>Quý này</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>Tuần này</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>Năm nay</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>Hôm nay</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>Những nghệ sĩ hàng đầu</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>Các bài hát hàng đầu</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>Nguồn hàng đầu</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Thời gian tổng cộng</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Các bài hát đã phát</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>Thoát chương trình</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="Error" xml:space="preserve">
<value>错误</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>导出成功</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>正在准备清理缓存...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>就绪</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>已检测到根目录路径。全磁盘索引可能包含大量非媒体文件,导致扫描时间过长。建议指定一个特定的子目录。</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>所有音乐</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>正在同步媒体库...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>媒体库同步出现问题</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>清除播放队列</value>
</data>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>关闭歌词窗口时退出程序</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>导出播放记录</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>导出设置</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>设置管理器</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>播放记录</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>浏览在线资源共享中心</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>启动</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>统计数据</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>关闭音乐库窗口时停止播放</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>标准模式</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>播放源</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>本月</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>本季度</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>本周</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>本年度</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>今日</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>最热爱的艺人</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>最喜欢的歌曲</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>最常用的播放源</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>总时长</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>已播放的曲目数</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>退出程序</value>
</data>

View File

@@ -159,6 +159,9 @@
<data name="DockedMode" xml:space="preserve">
<value>停靠模式</value>
</data>
<data name="Error" xml:space="preserve">
<value>錯誤</value>
</data>
<data name="ExportSettingsSuccess" xml:space="preserve">
<value>匯出成功</value>
</data>
@@ -183,6 +186,9 @@
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>準備清除快取...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>準備就緒</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>已偵測到根目錄路徑。全磁碟索引可能包含大量非媒體檔案,導致掃描時間過長。建議指定特定的子目錄。</value>
</data>
@@ -408,6 +414,12 @@
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>所有音樂</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>媒體庫同步中...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>媒體庫同步有問題</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>清除播放佇列</value>
</data>
@@ -514,7 +526,7 @@
<value>自動同步頻率</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>不</value>
<value>不</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
<value>每日</value>
@@ -846,6 +858,9 @@
<data name="SettingsPageExitOnLyricsWindowClosed.Header" xml:space="preserve">
<value>關閉歌詞視窗時結束程式</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>匯出播放記錄</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>匯出設定</value>
</data>
@@ -1299,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>設定管理器</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>播放記錄</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>瀏覽線上資源共享中心</value>
</data>
@@ -1374,6 +1392,9 @@
<data name="SettingsPageStartup.Text" xml:space="preserve">
<value>啟動</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>統計資料</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>關閉音樂庫視窗時停止播放</value>
</data>
@@ -1446,6 +1467,39 @@
<data name="StandardMode" xml:space="preserve">
<value>標準模式</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>來源</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<value>本月</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>本季</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<value>本週</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>今年</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>今天</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>頂級藝術家</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>熱門曲目</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>頂端來源</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>總時間</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>播放曲目</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>結束程式</value>
</data>

View File

@@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml.Controls;
using System;
using System.IO;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
@@ -74,6 +75,19 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
[RelayCommand]
private async Task ExportPlayHistoryAsync()
{
var folder = await PickerHelper.PickSingleFolderAsync<SettingsWindow>();
if (folder != null)
{
var dest = Path.Combine(folder.Path, $"BetterLyrics_Play_History_Export_{DateTime.Now:yyyyMMdd_HHmmss}.db");
await FileHelper.CopyFileAsync(PathHelper.PlayHistoryPath, dest);
ToastHelper.ShowToast("ExportSettingsSuccess", null, InfoBarSeverity.Success);
}
}
[RelayCommand]
private void ClearCacheFiles()
{

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Controls;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
@@ -9,6 +10,7 @@ using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI.Animations;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
@@ -51,23 +53,25 @@ namespace BetterLyrics.WinUI3.ViewModels
public void SyncFolder(MediaFolder folder)
{
if (folder.IsIndexing) return;
if (folder.IsProcessing) return;
_ = Task.Run(async () => await _fileSystemService.ScanMediaFolderAsync(folder, CancellationToken.None));
}
[RelayCommand]
private async Task AddMediaSourceAsync(string protocolType)
private async Task AddMediaSourceAsync(string fileSourceTypeName)
{
FileSourceType fileSourceType = Enum.Parse<FileSourceType>(fileSourceTypeName);
var dialog = new ContentDialog
{
XamlRoot = WindowHook.GetWindow<SettingsWindow>()?.Content.XamlRoot,
Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style,
Title = protocolType == "Local" ? _localizationService.GetLocalizedString("MediaSettingsControlLocalFolder") : protocolType,
Title = fileSourceType == FileSourceType.Local ? _localizationService.GetLocalizedString("MediaSettingsControlLocalFolder") : Enum.GetName(fileSourceType),
PrimaryButtonText = _localizationService.GetLocalizedString("Add"),
CloseButtonText = _localizationService.GetLocalizedString("Cancel"),
DefaultButton = ContentDialogButton.Primary,
Content = new RemoteServerConfigControl(protocolType)
Content = new RemoteServerConfigControl(fileSourceType)
};
dialog.PrimaryButtonClick += async (s, e) =>
@@ -86,13 +90,13 @@ namespace BetterLyrics.WinUI3.ViewModels
{
var tempFolder = configControl.GetConfig();
if (protocolType == "Local")
if (fileSourceType == FileSourceType.Local)
{
string path = tempFolder.UriPath;
if (!System.IO.Directory.Exists(path))
if (!Directory.Exists(path))
{
throw new System.IO.DirectoryNotFoundException(_localizationService.GetLocalizedString("RemoteServerConfigControlPathNotExisted"));
throw new DirectoryNotFoundException(_localizationService.GetLocalizedString("RemoteServerConfigControlPathNotExisted"));
}
var normalizedPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
@@ -126,6 +130,29 @@ namespace BetterLyrics.WinUI3.ViewModels
}
else
{
if (fileSourceType == FileSourceType.WebDAV)
{
// 使用辅助类探测协议
string? detectedScheme = await WebDavProbeHelper.DetectSchemeAsync(
tempFolder.UriHost,
tempFolder.UriPort,
tempFolder.UriPath,
tempFolder.UserName,
tempFolder.Password
);
if (detectedScheme == null)
{
// 探测失败,直接报错返回
configControl.ShowError(_localizationService.GetLocalizedString("SettingsPageServerTestFailedInfo"));
deferral.Complete();
return;
}
// 将探测到的正确协议 (http 或 https) 写入配置对象
tempFolder.UriScheme = detectedScheme;
}
var newUriString = tempFolder.GetStandardUri().AbsoluteUri.TrimEnd('/') + "/";
foreach (var existingFolder in AppSettings.LocalMediaFolders)
@@ -208,10 +235,5 @@ namespace BetterLyrics.WinUI3.ViewModels
await dialog.ShowAsync();
}
private void ShowErrorTip(RemoteServerConfigControl control, string message)
{
control.ShowError(message);
}
}
}

View File

@@ -92,7 +92,8 @@ namespace BetterLyrics.WinUI3.ViewModels
public SongsTabInfo? SelectedSongsTabInfo => AppSettings.StarredPlaylists.ElementAtOrDefault(SelectedSongsTabInfoIndex);
[ObservableProperty] public partial bool IsDataLoading { get; set; } = false;
[ObservableProperty] public partial bool IsDataSyncing { get; set; } = false;
[ObservableProperty] public partial bool IsDataSyncError { get; set; } = false;
[ObservableProperty] public partial ExtendedTrack TrackRightTapped { get; set; } = new();
@@ -121,6 +122,7 @@ namespace BetterLyrics.WinUI3.ViewModels
RefreshSongs();
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
_settingsService.AppSettings.LocalMediaFolders.ItemPropertyChanged += LocalMediaFolders_ItemPropertyChanged;
_mediaPlayer.MediaOpened += MediaPlayer_MediaOpened;
_mediaPlayer.MediaEnded += MediaPlayer_MediaEnded;
@@ -138,6 +140,11 @@ namespace BetterLyrics.WinUI3.ViewModels
_smtc.PlaybackPositionChangeRequested += Smtc_PlaybackPositionChangeRequested;
}
private void LocalMediaFolders_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
{
IsDataSyncError = AppSettings.LocalMediaFolders.Any(x => x.StatusSeverity == InfoBarSeverity.Error);
}
private void TrackPlayingQueue_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
AppSettings.MusicGallerySettings.PlayQueuePaths = [.. TrackPlayingQueue.Select(x => x.Track.DecodedAbsoluteUri)];
@@ -274,8 +281,6 @@ namespace BetterLyrics.WinUI3.ViewModels
{
_refreshSongsTimer.Debounce(() =>
{
IsDataLoading = true;
_ = Task.Run(async () =>
{
var enabledFolderIds = _settingsService.AppSettings.LocalMediaFolders
@@ -304,8 +309,6 @@ namespace BetterLyrics.WinUI3.ViewModels
IsLocalMediaNotFound = !_filteredTracks.Any();
ApplySongOrderType();
IsDataLoading = false;
});
});
}, Time.DebounceTimeout);
@@ -491,8 +494,7 @@ namespace BetterLyrics.WinUI3.ViewModels
if (targetFolder == null)
{
throw new Exception($"找不到文件 {PlayingTrack.FileName} 对应的存储配置。请检查服务器设置是否已启用。");
throw new FileNotFoundException(null, PlayingTrack.DecodedAbsoluteUri);
}
_currentProvider = targetFolder.CreateFileSystem();
@@ -500,7 +502,7 @@ namespace BetterLyrics.WinUI3.ViewModels
await _currentProvider.ConnectAsync();
var fileCacheStub = new FileCacheEntity
var fileCacheStub = new FilesIndexItem
{
Uri = PlayingTrack.Uri
};
@@ -509,7 +511,7 @@ namespace BetterLyrics.WinUI3.ViewModels
if (sourceStream == null)
{
throw new FileNotFoundException("无法打开文件流");
throw new FileNotFoundException(null, fileCacheStub.Uri);
}
if (sourceStream.CanSeek)
@@ -561,7 +563,7 @@ namespace BetterLyrics.WinUI3.ViewModels
}
catch (Exception ex)
{
ToastHelper.ShowToast($"PlayTrackAsync: Error", ex.Message, InfoBarSeverity.Error);
ToastHelper.ShowToast("Error", ex.Message, InfoBarSeverity.Error);
_timelineController.Pause();
}
}
@@ -634,12 +636,6 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
[RelayCommand]
private void SwitchPlaybackOrder()
{
AppSettings.MusicGallerySettings.PlaybackOrder = AppSettings.MusicGallerySettings.PlaybackOrder.GetNext();
}
[RelayCommand]
private async Task StopTrackAsync()
{
@@ -665,6 +661,10 @@ namespace BetterLyrics.WinUI3.ViewModels
{
RefreshSongs();
}
else if (message.PropertyName == nameof(MediaFolder.IsProcessing))
{
IsDataSyncing = message.NewValue;
}
}
}

View File

@@ -0,0 +1,148 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Stats;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class StatsDashboardControlViewModel : ObservableObject
{
private readonly IPlayHistoryService _playHistoryService;
public StatsDashboardControlViewModel(IPlayHistoryService playHistoryService)
{
_playHistoryService = playHistoryService;
}
[ObservableProperty] public partial bool IsLoading { get; set; }
[ObservableProperty] public partial TimeSpan TotalDuration { get; set; }
[ObservableProperty] public partial int TotalTracksPlayed { get; set; }
[ObservableProperty] public partial string TopPlayerName { get; set; } = "N/A";
public ObservableCollection<SongPlayCount> TopSongs { get; } = new();
public ObservableCollection<ArtistPlayCount> TopArtists { get; } = new();
public ObservableCollection<PlayerStatDisplayItem> PlayerStats { get; } = new();
/// <summary>
/// 核心方法:根据选中的 Tab 加载数据
/// </summary>
[RelayCommand]
public async Task LoadDataAsync(StatsRange range)
{
if (IsLoading) return;
IsLoading = true;
try
{
var (start, end) = CalculateDateRange(range);
var durationTask = _playHistoryService.GetTotalListeningDurationAsync(start, end);
var logsTask = _playHistoryService.GetLogsByDateRangeAsync(start, end);
var topSongsTask = _playHistoryService.GetTopSongsAsync(start, end, 10);
var topArtistsTask = _playHistoryService.GetTopArtistsAsync(start, end, 10);
var playersTask = _playHistoryService.GetPlayerDistributionAsync(start, end);
await Task.WhenAll(durationTask, logsTask, topSongsTask, topArtistsTask, playersTask);
TotalDuration = await durationTask;
var logs = await logsTask;
TotalTracksPlayed = logs.Count;
TopSongs.Clear();
foreach (var item in await topSongsTask) TopSongs.Add(item);
TopArtists.Clear();
foreach (var item in await topArtistsTask) TopArtists.Add(item);
UpdatePlayerStats(await playersTask);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error loading stats: {ex.Message}");
}
finally
{
IsLoading = false;
}
}
[RelayCommand]
private async Task GenerateTestDataAsync()
{
await _playHistoryService.GenerateTestDataAsync(10000);
}
/// <summary>
/// 将原始统计数据转换为带进度条宽度的 UI 数据
/// </summary>
private void UpdatePlayerStats(List<PlayerStats> stats)
{
PlayerStats.Clear();
if (stats == null || stats.Count == 0)
{
TopPlayerName = "N/A";
return;
}
double maxCount = stats.Max(x => x.Count);
if (maxCount == 0) maxCount = 1;
var topPlayer = stats.OrderByDescending(x => x.Count).FirstOrDefault();
TopPlayerName = PlayerIdHelper.GetDisplayName(topPlayer?.PlayerId) ?? "N/A";
foreach (var item in stats.OrderByDescending(x => x.Count))
{
PlayerStats.Add(new PlayerStatDisplayItem
{
PlayerId = item.PlayerId,
PlayCount = item.Count,
});
}
}
private (DateTime Start, DateTime End) CalculateDateRange(StatsRange range)
{
DateTime nowLocal = DateTime.Now;
DateTime startLocal = nowLocal.Date; // 默认为本地今天 00:00
switch (range)
{
case StatsRange.Day:
break;
case StatsRange.Week:
int dayOfWeek = (int)nowLocal.DayOfWeek;
if (dayOfWeek == 0) dayOfWeek = 7; // 处理周日
startLocal = nowLocal.Date.AddDays(-(dayOfWeek - 1));
break;
case StatsRange.Month:
startLocal = new DateTime(nowLocal.Year, nowLocal.Month, 1);
break;
case StatsRange.Quarter:
int quarterStartMonth = (nowLocal.Month - 1) / 3 * 3 + 1;
startLocal = new DateTime(nowLocal.Year, quarterStartMonth, 1);
break;
case StatsRange.Year:
startLocal = new DateTime(nowLocal.Year, 1, 1);
break;
}
// 数据库里的 StartedAt 是 UTC所以查询条件必须也是 UTC
DateTime startUtc = startLocal.ToUniversalTime();
DateTime endUtc = nowLocal.ToUniversalTime();
return (startUtc, endUtc);
}
}
}

View File

@@ -29,14 +29,14 @@
</Page.Resources>
<Grid>
<Grid Padding="12,16,12,64" ColumnSpacing="12">
<Grid Padding="12,4,12,68" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0">
<ScrollViewer x:Name="LeftSidePanel" Grid.Column="0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -49,7 +49,7 @@
<TextBlock
x:Uid="MusicGalleryPagePlaylist"
Grid.Row="0"
Margin="1,0,0,6"
Margin="1,8,0,6"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<Grid Grid.Row="1">
@@ -98,7 +98,7 @@
Click="RemoveFromPlaylistButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE711;}"
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind IsDefault, Converter={StaticResource BoolNegationToVisibilityConverter}}">
<ToolTipService.ToolTip>
@@ -192,8 +192,11 @@
</Grid>
</ScrollViewer>
<Grid x:Name="SongViewer" Grid.Column="1">
<controls:ContentSizer Grid.Column="1" TargetControl="{x:Bind LeftSidePanel}" />
<Grid x:Name="SongViewer" Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
@@ -249,12 +252,12 @@
<AutoSuggestBox
x:Name="SongSearchBox"
x:Uid="MusicGalleryPageSongSearchBox"
Margin="0,0,128,0"
HorizontalAlignment="Stretch"
QueryIcon="Find"
Text="{x:Bind ViewModel.SongSearchQuery, Mode=TwoWay}" />
<Grid>
<StackPanel
HorizontalAlignment="Left"
Orientation="Horizontal"
@@ -302,7 +305,20 @@
</StackPanel>
<SemanticZoom Grid.Row="1">
<InfoBar
x:Uid="MusicGalleryPageDataSync"
Grid.Row="1"
IsClosable="False"
IsOpen="{x:Bind ViewModel.IsDataSyncing, Mode=OneWay}" />
<InfoBar
x:Uid="MusicGalleryPageDataSyncError"
Grid.Row="1"
IsClosable="False"
IsOpen="{x:Bind ViewModel.IsDataSyncError, Mode=OneWay}"
Severity="Error" />
<SemanticZoom Grid.Row="2">
<SemanticZoom.ZoomedInView>
<ListView
x:Name="SongListView"
@@ -310,39 +326,15 @@
SelectionChanged="SongListView_SelectionChanged"
SelectionMode="Multiple">
<ListView.ContextFlyout>
<MenuBarItemFlyout>
<MenuBarItemFlyout Opened="AddToMenuBarItemFlyout_Opened">
<MenuFlyoutSubItem x:Uid="MusicGalleryPageAddToPlayingQueue" IsEnabled="{x:Bind ViewModel.SelectedTracks.Count, Mode=OneWay, Converter={StaticResource IntToBoolConverter}}">
<MenuFlyoutItem x:Uid="MusicGalleryPageAddToNext" Click="AddSongToQueueNextMenuFlyoutItem_Click" />
<MenuFlyoutItem x:Uid="MusicGalleryPageAddToEnd" Click="AddSongToQueueEndMenuFlyoutItem_Click" />
</MenuFlyoutSubItem>
<MenuFlyoutItem
<MenuFlyoutSubItem
x:Name="AddToCustomListMenuFlyoutSubItem"
x:Uid="MusicGalleryPageAddToCustomList"
Click="AddToPlaylistMenuFlyoutItem_Click"
IsEnabled="{x:Bind ViewModel.SelectedTracks.Count, Mode=OneWay, Converter={StaticResource IntToBoolConverter}}">
<MenuFlyoutItem.ContextFlyout>
<Flyout FlyoutPresenterStyle="{StaticResource FlyoutGhostStyle}" Placement="Bottom">
<ListView ItemsSource="{x:Bind ViewModel.AppSettings.StarredPlaylists, Mode=OneWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:SongsTabInfo">
<Grid Tapped="ToBeAddedPlaylistsListViewItemGrid_Tapped">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="{x:Bind Icon}" />
<TextBlock
Margin="0,0,0,2"
VerticalAlignment="Center"
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind Name}" />
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Flyout>
</MenuFlyoutItem.ContextFlyout>
</MenuFlyoutItem>
IsEnabled="{x:Bind ViewModel.SelectedTracks.Count, Mode=OneWay, Converter={StaticResource IntToBoolConverter}}" />
</MenuBarItemFlyout>
</ListView.ContextFlyout>
<ListView.ItemTemplate>
@@ -448,7 +440,7 @@
</SemanticZoom.ZoomedOutView>
</SemanticZoom>
<Grid Grid.Row="1" Visibility="{x:Bind ViewModel.IsLocalMediaNotFound, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid Grid.Row="2" Visibility="{x:Bind ViewModel.IsLocalMediaNotFound, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
@@ -463,264 +455,157 @@
</Grid>
<Grid x:Name="PlayQueue" Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
</Grid>
<Grid Grid.Row="0">
<Grid
x:Name="PlayQueue"
Width="300"
Margin="0,4,4,72"
Padding="12,16,12,0"
HorizontalAlignment="Right"
Background="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
BorderThickness="1"
CornerRadius="8"
Translation="310,0,0">
<Grid.TranslationTransition>
<Vector3Transition />
</Grid.TranslationTransition>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBlock
x:Uid="MusicGalleryPagePlayingQueue"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" />
</Grid>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=OneWay, Converter={StaticResource IndexToDisplayConverter}}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="/" />
<TextBlock Text="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}" />
</StackPanel>
<!-- Stop media session -->
<Button
Grid.Column="1"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.StopTrackCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE71A;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageStopTrack" />
</ToolTipService.ToolTip>
</Button>
<!-- Scroll to playing item -->
<Button
Grid.Column="3"
HorizontalAlignment="Right"
Click="ScrollToPlayingItemButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE7B7;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageScrollToPlayingItem" />
</ToolTipService.ToolTip>
</Button>
<!-- Empty play queue -->
<Button
Grid.Column="4"
HorizontalAlignment="Right"
Click="EmptyPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageEmptyPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
<ListView
x:Name="PlayingQueueListView"
Grid.Row="3"
ItemsSource="{x:Bind ViewModel.TrackPlayingQueue, Mode=OneWay}"
SelectedIndex="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Padding="0,6">
<Grid Tapped="PlayingQueueListVireItemGrid_Tapped">
<StackPanel Margin="0,0,36,0">
<TextBlock Text="{Binding Track.Title}" TextWrapping="Wrap" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Track.Artist}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
<Grid HorizontalAlignment="Right">
<Button
Click="RemoveFromPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageRemoveFromPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Grid.Row="3">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="100" Source="/Assets/EmptyBox.png" />
<TextBlock
x:Uid="MusicGalleryPagePlayingQueue"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" />
</Grid>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=OneWay, Converter={StaticResource IndexToDisplayConverter}}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="/" />
<TextBlock Text="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}" />
</StackPanel>
<!-- Stop media session -->
<Button
Grid.Column="1"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.StopTrackCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE71A;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageStopTrack" />
</ToolTipService.ToolTip>
</Button>
<!-- Playback order -->
<Button
Grid.Column="2"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.SwitchPlaybackOrderCommand}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<ToolTip>
<Grid>
<TextBlock x:Name="PlaybackRepeatAllHint" x:Uid="MusicGalleryPageQueueLoop" />
<TextBlock x:Name="PlaybackRepeatOneHint" x:Uid="MusicGalleryPageSingleLoop" />
<TextBlock x:Name="PlaybackShuffleHint" x:Uid="MusicGalleryPageQueueRandom" />
</Grid>
</ToolTip>
</ToolTipService.ToolTip>
<Button.Content>
<Grid>
<!-- Repeat all -->
<FontIcon
x:Name="PlaybackRepeatAll"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8EE;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Repeat one -->
<FontIcon
x:Name="PlaybackRepeatOne"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8ED;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
<!-- Shuffle -->
<FontIcon
x:Name="PlaybackShuffle"
FontFamily="{StaticResource IconFontFamily}"
FontSize="16"
Glyph="&#xE8B1;">
<FontIcon.OpacityTransition>
<ScalarTransition />
</FontIcon.OpacityTransition>
</FontIcon>
</Grid>
</Button.Content>
</Button>
<!-- Scroll to playing item -->
<Button
Grid.Column="3"
HorizontalAlignment="Right"
Click="ScrollToPlayingItemButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE7B7;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageScrollToPlayingItem" />
</ToolTipService.ToolTip>
</Button>
<!-- Empty play queue -->
<Button
Grid.Column="4"
HorizontalAlignment="Right"
Click="EmptyPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageEmptyPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
<ListView
x:Name="PlayingQueueListView"
Grid.Row="3"
ItemsSource="{x:Bind ViewModel.TrackPlayingQueue, Mode=OneWay}"
SelectedIndex="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Padding="0,6">
<Grid Tapped="PlayingQueueListVireItemGrid_Tapped">
<StackPanel>
<TextBlock Text="{Binding Track.Title}" TextWrapping="Wrap" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Track.Artist}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
<Grid HorizontalAlignment="Right">
<Button
Click="RemoveFromPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageRemoveFromPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Grid.Row="3">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel
x:Uid="MusicGalleryPagePlayingQueueEmpty"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="100" Source="/Assets/EmptyBox.png" />
<TextBlock
x:Uid="MusicGalleryPagePlayingQueueEmpty"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Grid>
</Grid>
<Grid Background="{ThemeResource SolidBackgroundFillColorBaseBrush}" Visibility="{x:Bind ViewModel.IsDataLoading, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="12" />
<RowDefinition Height="*" />
<RowDefinition Height="12" />
<RowDefinition Height="*" />
<RowDefinition Height="12" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<labs:Shimmer Grid.Row="0" CornerRadius="6" />
<labs:Shimmer Grid.Row="2" CornerRadius="6" />
<labs:Shimmer Grid.Row="4" CornerRadius="6" />
<labs:Shimmer Grid.Row="6" CornerRadius="6" />
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="PlaybackOrderState">
<VisualState x:Name="RepeatAll">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="1" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="0" />
<Setter Target="PlaybackShuffle.Opacity" Value="0" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Visible" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="RepeatOne">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="1" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="0" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="1" />
<Setter Target="PlaybackShuffle.Opacity" Value="0" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Visible" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Shuffle">
<VisualState.StateTriggers>
<ui:CompareStateTrigger
Comparison="Equal"
Value="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlaybackOrder, Mode=OneWay, Converter={StaticResource EnumToIntConverter}}"
To="2" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PlaybackRepeatAll.Opacity" Value="0" />
<Setter Target="PlaybackRepeatOne.Opacity" Value="0" />
<Setter Target="PlaybackShuffle.Opacity" Value="1" />
<Setter Target="PlaybackRepeatAllHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackRepeatOneHint.Visibility" Value="Collapsed" />
<Setter Target="PlaybackShuffleHint.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</Page>

View File

@@ -27,6 +27,15 @@ namespace BetterLyrics.WinUI3.Views
{
public MusicGalleryPageViewModel ViewModel => (MusicGalleryPageViewModel)DataContext;
public bool IsPlayingQueueOpened
{
get { return (bool)GetValue(IsPlayingQueueOpenedProperty); }
set { SetValue(IsPlayingQueueOpenedProperty, value); }
}
public static readonly DependencyProperty IsPlayingQueueOpenedProperty =
DependencyProperty.Register(nameof(IsPlayingQueueOpened), typeof(bool), typeof(MusicGalleryPage), new PropertyMetadata(false, OnDependencyPropertyChanged));
public MusicGalleryPage()
{
InitializeComponent();
@@ -34,6 +43,18 @@ namespace BetterLyrics.WinUI3.Views
ViewModel.AppSettings.MusicGallerySettings.PropertyChanged += MusicGallerySettings_PropertyChanged;
}
private static void OnDependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MusicGalleryPage self)
{
if (e.Property == IsPlayingQueueOpenedProperty)
{
var newValue = (bool)e.NewValue;
self.PlayQueue.Translation = newValue ? new() : new(310, 0, 0);
}
}
}
private void ScrollToPlayingItem()
{
if (ViewModel.PlayingQueueItem == null) return;
@@ -216,40 +237,6 @@ namespace BetterLyrics.WinUI3.Views
SongListView.SelectedItems.Clear();
}
private void AddToPlaylistMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
//((MenuFlyoutItem)sender).ContextFlyout.ShowAt(PlaylistButton);
}
private void ToBeAddedPlaylistsListViewItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
var songsTabInfo = ((SongsTabInfo)((FrameworkElement)sender).DataContext);
if (songsTabInfo.FilterProperty == CommonSongProperty.M3UFilePath)
{
if (songsTabInfo.FilterValue is string path)
{
if (File.Exists(path))
{
var content = File.ReadAllText(path);
foreach (var item in ViewModel.SelectedTracks.Select(x => x.DecodedAbsoluteUri).ToList())
{
if (!content.Contains(item))
{
content += Environment.NewLine;
content += item;
}
}
File.WriteAllText(path, content);
ToastHelper.ShowToast("TracksAddToPlaylistSuccessfully", null, InfoBarSeverity.Success);
}
else
{
ToastHelper.ShowToast("TracksAddToPlaylistFailed", null, InfoBarSeverity.Error);
}
}
}
}
private async void SongListViewItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
var displayedTracks = SongListView.Items.Cast<ExtendedTrack>();
@@ -282,5 +269,52 @@ namespace BetterLyrics.WinUI3.Views
ViewModel.SelectFolder(selectedFolder);
}
}
private void ToBeAddedPlaylistsMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
var songsTabInfo = ((SongsTabInfo)((FrameworkElement)sender).DataContext);
if (songsTabInfo.FilterProperty == CommonSongProperty.M3UFilePath)
{
if (songsTabInfo.FilterValue is string path)
{
if (File.Exists(path))
{
var content = File.ReadAllText(path);
foreach (var item in ViewModel.SelectedTracks.Select(x => x.DecodedAbsoluteUri).ToList())
{
if (!content.Contains(item))
{
content += Environment.NewLine;
content += item;
}
}
File.WriteAllText(path, content);
ToastHelper.ShowToast("TracksAddToPlaylistSuccessfully", null, InfoBarSeverity.Success);
}
else
{
ToastHelper.ShowToast("TracksAddToPlaylistFailed", null, InfoBarSeverity.Error);
}
}
}
}
private void AddToMenuBarItemFlyout_Opened(object sender, object e)
{
AddToCustomListMenuFlyoutSubItem.Items.Clear();
foreach (var item in ViewModel.AppSettings.StarredPlaylists)
{
if (item.FilterProperty == CommonSongProperty.M3UFilePath)
{
var menuFlyoutItem = new MenuFlyoutItem
{
Text = item.Name,
DataContext = item,
};
menuFlyoutItem.Click += ToBeAddedPlaylistsMenuFlyoutItem_Click;
AddToCustomListMenuFlyoutSubItem.Items.Add(menuFlyoutItem);
}
}
}
}
}

View File

@@ -16,7 +16,7 @@
Loaded="RootGrid_Loaded"
Unloaded="RootGrid_Unloaded">
<local:MusicGalleryPage x:Name="MusicGalleryPage" />
<local:MusicGalleryPage x:Name="MusicGalleryPage" IsPlayingQueueOpened="{Binding ElementName=NowPlayingBar, Path=IsPlayingQueueOpened, Mode=OneWay}" />
<local:NowPlayingPage
x:Name="NowPlayingPage"
@@ -32,6 +32,9 @@
x:Name="NowPlayingBar"
VerticalAlignment="Bottom"
IsAutoHideEnabled="False"
PlaybackOrder="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlaybackOrder, Mode=TwoWay}"
ShowPlaybackOrderButton="True"
ShowPlayingQueueButton="True"
ShowSongInfo="True"
SongInfoTapped="NowPlayingBar_SongInfoTapped"
TimeTapped="NowPlayingBar_TimeTapped" />

View File

@@ -1,3 +1,4 @@
using BetterLyrics.WinUI3.Controls;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Hooks;
@@ -127,5 +128,10 @@ namespace BetterLyrics.WinUI3.Views
{
ViewModel.AppSettings.MusicGallerySettings.LyricsWindowStatus.IsOpened = false;
}
private void NowPlayingBar_PlayingQueueClick(object sender, System.EventArgs e)
{
MusicGalleryPage.IsPlayingQueueOpened = !MusicGalleryPage.IsPlayingQueueOpened;
}
}
}

View File

@@ -50,6 +50,12 @@
Glyph=&#xEA69;}"
Tag="PlaybackLib" />
<NavigationViewItem
x:Uid="SettingsPageStats"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE9D2;}"
Tag="Stats" />
<NavigationViewItem
x:Uid="SettingsPageAbout"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -85,6 +91,11 @@
<uc:MediaSettingsControl />
</controls:Case>
<!-- Stats -->
<controls:Case Value="Stats">
<uc:StatsDashboardControl />
</controls:Case>
<!-- About -->
<controls:Case Value="About">
<uc:AboutControl />

BIN
Promotion/banner_fade.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

View File

@@ -1,235 +1,183 @@
![](Promotion/banner.png)
<div align=center>
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="96">
</div>
<h2 align=center>
BetterLyrics
</h2>
<h4 align="center">
🤩 一款优雅且高度自定义的歌词/播放器应用,基于 WinUI3/Win2D 构建
</h4>
[**中文**](README.CN.md) | [**English**](README.md)
<div align="center">
[使用指南](https://github.com/jayfunc/BetterLyrics/wiki/使用指南) | [隐私政策](PrivacyPolicy.CN.md) | [服务协议](TermsofService.CN.md)
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="Logo" width="120">
<h1>BetterLyrics</h1>
<h4>
🤩 一款优雅且高度自定义的歌词可视化与全能音乐播放应用 <br>
基于 WinUI3 / Win2D 构建
</h4>
<div>
<img src="https://img.shields.io/badge/Language-C%23-purple" alt="C#">
<img src="https://img.shields.io/badge/Framework-WinUI%203-blue" alt="WinUI 3">
<img src="https://img.shields.io/badge/License-GPL_v3.0-blue" alt="License">
<a href="https://github.com/jayfunc/BetterLyrics/stargazers"><img src="https://img.shields.io/github/stars/jayfunc/BetterLyrics" alt="Stars"></a>
<a href="https://crowdin.com/project/betterlyrics"><img src="https://badges.crowdin.net/betterlyrics/localized.svg" alt="Crowdin"></a>
</div>
<br>
<img src="Promotion/banner.png" alt="Banner" width="100%" style="border-radius: 10px;">
</div>
<div align=center>
![Static Badge](https://img.shields.io/badge/Language-C%23-purple)
![Static Badge](https://img.shields.io/badge/License-GPL_v3.0-blue)
![Static Badge](https://img.shields.io/badge/IDE-Visual%20Studio-purple)
![Static Badge](https://img.shields.io/badge/Framework-WinUI%203-blue)
<br>
</div>
<div align=center>
[![GitHub Repo stars](https://img.shields.io/github/stars/jayfunc/BetterLyrics)](https://github.com/jayfunc/BetterLyrics/stargazers)
[![Crowdin](https://badges.crowdin.net/betterlyrics/localized.svg)](https://crowdin.com/project/betterlyrics)
</div>
<div align=center>
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics)
</div>
## 🔥 精选推荐与社区
<div align="center">
<mark>**_💞 BetterLyrics 的发展离不开每一位贡献者、反馈者和用户的全力支持。_**</mark>
| HelloGitHub 推荐 | 少数派 SSPAI 推荐 | 🤖 AI 问答 |
| :---: | :---: | :---: |
| <a href="https://hellogithub.com/repository/jayfunc/BetterLyrics" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d2af74f0aea146ad8e4b2086982f5777&claim_uid=SgtQs9c54C8wjnv" alt="HelloGitHub" height="40"></a> | [**阅读评测文章**](https://sspai.com/post/101028) | [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics) <br> [![Zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTk5QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics) |
<mark>**_项目持续活跃开发中可能会遇到未知问题。_**</mark>
**交流群:** [QQ 群 (1054700388)](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
</div>
## ✍️ 协助翻译
## 🧪 下载与安装
找不到你的语言?有更好的翻译?没关系!😆 访问 [此处](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file) 查看如何贡献翻译!
<div align="center">
## 🎉 该项目入选少数派推荐文章!
| Microsoft Store (推荐) | 手动安装 |
| :---: | :---: |
| <a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" width="160"/></a><br>无限期免费试用(功能与付费版一致) | [**📦 最新版本 (.zip)**](https://github.com/jayfunc/BetterLyrics/releases/latest)<br>[查看安装指南](https://www.cnblogs.com/jayfunc/p/19212078) |
文章链接:[BetterLyrics - 一款专为 Windows 打造的沉浸式流畅歌词显示软件](https://sspai.com/post/101028)
[📖 用户指南](https://github.com/jayfunc/BetterLyrics/wiki/使用指南) | [🔒 隐私政策](PrivacyPolicy.CN.md) | [⚖️ 服务条款](TermsofService.CN.md)
## 🔈 反馈交流群
</div>
[QQ 群](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388) | [Discord Server](https://discord.gg/5yAQPnyCKv) | [Telegram Group](https://t.me/+svhSLZ7awPsxNGY1)
## 🌟 核心功能
## 🌟 特色功能
- 🎨 **绝美视觉与 UI**
- **优雅设计:** 基于 WinUI3 & Win2D 的流畅、高度个性化风格。
- **沉浸特效:** 支持流体背景、3D/扇形歌词、雪花粒子等多种效果。
- **深度定制:** 按需配置动画、字体和行为逻辑,打造你的专属播放器。
- 🌠 **精美的用户界面**
- 流畅、高度自定义的样式、动画、动效
- 沉浸式流体背景
- 透视/扇形歌词
- 雪花效果
- 多种歌词滚动函数
- ...
- ↔️ **强大的歌词翻译**
- 本地机器翻译 (支持 30 多种语言)
- 自动读取本地音乐文件内嵌歌词
- 🧩 **多种歌词源**
- 💾 本地源
- 音乐文件 (内嵌歌词)
- [.lrc](<https://en.wikipedia.org/wiki/LRC_(file_format)>) 文件 (传统格式、增强格式)
- [.eslrc](https://github.com/ESLyric/release) 文件
- [.ttml](https://en.wikipedia.org/wiki/Timed_Text_Markup_Language) 文件
- ☁️ 在线源
- QQ 音乐
- 网易云音乐
- 酷狗音乐
- [amll-ttml-db](https://github.com/Steve-xmh/amll-ttml-db)
- [LRCLIB](https://lrclib.net/)
- <details><summary>⚠️ Apple Music (需要额外配置)</summary>
- 🎧 **多功能播放与连接**
- **内置播放器:** 支持播放 **本地硬盘** 文件或通过 **网络协议** (SMB, WebDAV) 流式播放。
- **外部集成:** 可视化来自 Spotify, Apple Music, 网易云音乐及 [其他多种播放器](https://github.com/jayfunc/BetterLyrics/wiki/使用指南#已知支持的音乐播放器配置指南) 的音乐。
- 浏览器打开 Apple Music打开开发者工具。刷新网页回到开发者工具窗口筛选出 Fetch/XHR选择一个请求在请求标头中找到 media-user-token 并复制其值。
- 打开 BetterLyrics 转到播放源设置。在 Media-User-Token (for Apple Music) 中粘贴复制的值并点按右侧对勾
- 🎶 **支持众多音乐播放器**
- 🌐 **强大的歌词系统**
- **离线翻译:** 注重隐私的本地机器翻译(支持 30+ 种语言)
- **全面源支持:** 支持 .lrc (标准/增强), .eslrc, .ttml, 内嵌标签以及在线源QQ 音乐, 网易云, LRCLIB
- **Apple Music** 支持歌词获取(需配置 Token
- 点击 [此处](https://github.com/jayfunc/BetterLyrics/wiki/使用指南#已知支持的音乐播放器配置指南) 查看详细信息
- 🪟 **全场景显示模式**
- **标准模式:** 全屏沉浸式体验。
- **停靠模式:** 贴附于屏幕边缘的精致侧边栏。
- **桌面悬浮:** 悬浮于所有应用之上的歌词挂件。
- 🪟 **多种显示模式**
- **标准模式**
- 标准的歌词窗口样式,沉浸式的音乐歌词体验。
- **停靠模式**
- 停靠在屏幕上/下边缘的轻量歌词窗口,工作休闲互不打扰。
- **桌面模式**
- 悬浮在所有应用上层,不能被选中,但能直击你的使用需求。
- **更多模式...**
- 等你来发现...
- 🧠 **智能化行为**
- 根据歌曲播放状态自动显隐歌词窗口
- 🧠 **智能行为**
- 音乐暂停时自动隐藏。
## 🖼️ 屏幕截图
## 🖼️ 软件截图
![](Screenshots/fs2.png)
![](Screenshots/std.png)
![](Screenshots/narrow.png)
![](Screenshots/Snipaste_2025-10-31_19-23-17.png)
![](Screenshots/Snipaste_2025-10-31_19-27-34.png)
![](Screenshots/dock.png)
![](Screenshots/desktop.png)
<div align="center">
> ⚠️ 由于 GIF 格式帧率限制,效果仅作展示。请以实机效果为准。
| 标准视图 | 侧边栏模式 |
| :---: | :---: |
| <img src="Screenshots/std.png" width="100%"> | <img src="Screenshots/narrow.png" width="100%"> |
![](Screenshots/PixPin_2025-10-24_18-13-44.gif)
![](Screenshots/PixPin_2025-10-24_18-17-17.gif)
| 歌词视觉特效 | 多模式共存 |
| :---: | :---: |
| <img src="Screenshots/effect.png" width="100%"> | <img src="Screenshots/all-in-one.png" width="100%"> |
## 📹 演示
| 全屏模式 | 全屏模式 |
| :---: | :---: |
| <img src="Screenshots/fs3.png" width="100%"> | <img src="Screenshots/fs2.png" width="100%"> |
在 [哔哩哔哩](https://www.bilibili.com/video/BV1QRstz1EGt/) 上观看于 2025 年 10 月 21 日上传的演示视频。
| 音乐库 | 播放统计 |
| :---: | :---: |
| <img src="Screenshots/music-gallery.png" width="100%"> | <img src="Screenshots/stats.png" width="100%"> |
## 🧪 即刻体验
</div>
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/zh-cn%20dark.svg" width="200"/>
</a>
## 📹 演示视频
**无限期**免费试用版和付费版**无任何区别**
> 观看我们在 Bilibili 发布的演示视频(上传于 2025 年 10 月 21 日):[点击观看](https://www.bilibili.com/video/BV1QRstz1EGt/)
如果喜欢该软件,请考虑 [捐赠](#-捐赠) 或在 **Microsoft Store** 购买,感谢您的支持! 🥰
## ✍️ 贡献与构建
无法从 Microsoft Store 下载?尝试以下方法:
- [从 Microsoft Store 外部下载](https://www.cnblogs.com/jayfunc/p/19212083)
- 转至 [最新发布页](https://github.com/jayfunc/BetterLyrics/releases/latest) 并从 `Assets`(资产)列表下载 `.zip` 文件。(安装方法参考 [此文档](https://www.cnblogs.com/jayfunc/p/19212078)。)
**协助翻译:** 找不到你的语言?[点此开始翻译](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file)。
## 🏗️ 构建
**从源码构建:**
> 构建前,请确保已替换 `Constants` 文件夹下的 `DiscordTemplate.cs` 和 `LastFM.cs`。
在构建之前确保:
- 替换文件 `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate``BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate.cs`.
- 替换文件 `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFMTemplate``BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFM.cs`
## 🤑 赞助与捐赠
## 🤑 捐赠
如果你喜欢 BetterLyrics请考虑支持它。你的支持有助于项目持续发展
如果你喜欢本应用,请考虑捐赠支持开发者。这将有助于本应用的长远发展。
<div align="center">
通过以下途径捐赠:
- [PayPal](https://paypal.me/zhefangpay)
- [Buy Me a Coffee](https://buymeacoffee.com/founchoo)
- [爱发电](https://afdian.com/a/jayfunc)
- <details><summary>支付宝</summary>
![](Donate/Alipay.jpg)
</detais>
| 网页平台 | 支付宝 (扫码) | 微信 (扫码) |
| :---: | :---: | :---: |
| [PayPal](https://paypal.me/zhefangpay)<br><br>[Buy Me a Coffee](https://buymeacoffee.com/founchoo)<br><br>[爱发电 (Afdian)](https://afdian.com/a/jayfunc) | <img src="Donate/Alipay.jpg" width="150"> | <img src="Donate/WeChatReward.png" width="150"> |
- <details><summary>微信</summary>
![](Donate/WeChatReward.png)
</details>
**[查看完整赞助者名单 (Hall of Fame)](SPONSORS.md)**
本项目的持续发展离不开大家的支持。**[查看完整鸣谢名单](SPONSORS.md)**
</div>
## 📄 许可证
## ⭐ Star 历史趋势
本项目采用 GNU 通用公共许可证 v3.0 授权。详情请参阅 [LICENSE](https://github.com/jayfunc/BetterLyrics/blob/dev/LICENSE) 文件。
<div align="center">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date" width="100%">
</div>
## 💖 感
## 📄 许可与致
部分功能及代码引用或修改自公开资料库,包括但不限于下述开源项目/包、教程等,在此一并感谢
本项目采用 **GNU General Public License v3.0** 许可证
<details>
<summary><b>💖 特别致谢、引用与灵感</b></summary>
<br>
**依赖与引用:**
| 项目/包 | 描述 |
| :--- | :--- |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | QQ、网易、酷狗在线歌词源提供歌词抓取、解密、解析等一系列方法 |
| [lrclib](https://github.com/tranxuanthang/lrclib) | LRCLIB 歌词 API |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | Apple Music 歌词抓取Python 实现) |
| [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet) | 从音乐文件提取图片 |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | 提供有关窗口的开箱即用的 Win32 API |
| [TagLib#](https://github.com/mono/taglib-sharp) | 读取音乐文件内嵌的原始歌词内容 |
| [Vanara](https://github.com/dahall/Vanara) | 提供开箱即用的 Win32 API |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | 离线翻译核心 |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | 动态流体背景 |
| [SpectrumVisualization](https://github.com/Johnwikix/SpectrumVisualization) | 频谱图 |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | 为 WinUI3 提供众多开箱即用的功能 |
| ... | ... |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | QQ、网易、酷狗歌词获取与解密 |
| [lrclib](https://github.com/tranxuanthang/lrclib) | LRCLIB 歌词 API 提供方 |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | Apple Music 歌词获取 |
| [Audio Tools Library (ATL)](https://github.com/Zeugma440/atldotnet) | 从音乐文件提取图片 |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | 简化 Win32 API 的窗口访问 |
| [TagLib#](https://github.com/mono/taglib-sharp) | 读取原始歌词内容 |
| [Vanara](https://github.com/dahall/Vanara) | Win32 API 封装库 |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | 离线歌词翻译支持 |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | 动态流体背景实现 |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | WinUI 3 开发辅助工具 |
点按 [此处](https://github.com/jayfunc/BetterLyrics/network/dependencies) 查看所有依赖
查看 [完整依赖列表](https://github.com/jayfunc/BetterLyrics/network/dependencies)。
### 教程、博客等
<br>
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
- [Bilibili -【WinUI3】SystemBackdropController定义云母、亚克力效果](https://www.bilibili.com/video/BV1PY4FevEkS)
- [cnblogs - .NET App 与 Windows 系统媒体控制(SMTC)交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 中的游戏循环CanvasAnimatedControl](https://www.cnblogs.com/walterlv/p/10236395.html)
- [r2d2rigo/Win2D-Samples](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
- [CommunityToolkit - 从入门到精通](https://mvvm.coldwind.top/)
## 💡 灵感来源
部分设计思路参考自下述插件/软件(不含间接或直接引用、修改的代码,仅作为设计思路指导方向)。
**💡 灵感来源:**
部分设计理念参考了以下插件/软件(仅作为设计思路参考,不涉及代码引用):
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
## ⭐ 星标记录
</details>
<div style="display: flex; justify-content: space-around; align-items: flex-start;">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date" width="100%" >
## 💭 分享到社交媒体
<details>
<summary><b>点击展开</b></summary>
<br>
<div align="center">
<img src="https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light" width="48%">
<img src="https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics" width="48%">
</div>
</details>
## 🤗 欢迎反馈问题、提交代码
<br>
如果发现 Bug 请在 Issues 内提出,同时也欢迎任何想法、建议。
## ⚠️ 免责声明
本项目按“原样”提供,不提供任何形式的担保。
所有歌词、字体、图标及其他第三方资源均为其各自版权所有者的财产。
本项目作者不主张对这些资源的所有权。
本项目为非商业用途,不得用于侵犯任何权利。
用户有责任确保其使用符合适用的法律和许可协议。
## 💭 社交媒体分享
![BetterLyrics](https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light)
![BetterLyrics](https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics)
<div align="center">
<mark><i>本项目正处于积极开发阶段;可能会出现意外问题。</i></mark><br>
<sub>免责声明:本项目“按原样”提供。所有第三方资源归其各自所有者所有。</sub>
</div>

302
README.md
View File

@@ -1,243 +1,183 @@
![](Promotion/banner.png)
<div align=center>
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="96">
</div>
<h2 align=center>
BetterLyrics
</h2>
<h4 align="center">
🤩 An elegant and deeply customizable lyrics & player app, built with WinUI3/Win2D
</h4>
[**中文**](README.CN.md) | [**English**](README.md)
<div align="center">
[User Guide](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide) | [Privacy Policy](PrivacyPolicy.md) | [Terms of Service](TermsofService.md)
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="Logo" width="120">
<h1>BetterLyrics</h1>
<h4>
🤩 An elegant and deeply customizable lyrics visualizer & versatile music player <br>
Built with WinUI3 / Win2D
</h4>
<div>
<img src="https://img.shields.io/badge/Language-C%23-purple" alt="C#">
<img src="https://img.shields.io/badge/Framework-WinUI%203-blue" alt="WinUI 3">
<img src="https://img.shields.io/badge/License-GPL_v3.0-blue" alt="License">
<a href="https://github.com/jayfunc/BetterLyrics/stargazers"><img src="https://img.shields.io/github/stars/jayfunc/BetterLyrics" alt="Stars"></a>
<a href="https://crowdin.com/project/betterlyrics"><img src="https://badges.crowdin.net/betterlyrics/localized.svg" alt="Crowdin"></a>
</div>
<br>
<img src="Promotion/banner.png" alt="Banner" width="100%" style="border-radius: 10px;">
</div>
<div align=center>
![Static Badge](https://img.shields.io/badge/Language-C%23-purple)
![Static Badge](https://img.shields.io/badge/License-GPL_v3.0-blue)
![Static Badge](https://img.shields.io/badge/IDE-Visual%20Studio-purple)
![Static Badge](https://img.shields.io/badge/Framework-WinUI%203-blue)
<br>
</div>
<div align=center>
[![GitHub Repo stars](https://img.shields.io/github/stars/jayfunc/BetterLyrics)](https://github.com/jayfunc/BetterLyrics/stargazers)
[![Crowdin](https://badges.crowdin.net/betterlyrics/localized.svg)](https://crowdin.com/project/betterlyrics)
</div>
<div align=center>
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics)
</div>
## 🔥 Featured & Community
<div align="center">
<mark>**_💞 BetterLyrics is made possible by all its contributors, bug reporters and users._**</mark>
| Featured by HelloGitHub | Featured by SSPAI | 🤖 Ask AI |
| :---: | :---: | :---: |
| <a href="https://hellogithub.com/repository/jayfunc/BetterLyrics" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d2af74f0aea146ad8e4b2086982f5777&claim_uid=SgtQs9c54C8wjnv" alt="HelloGitHub" height="40"></a> | [**Read the Review Article**](https://sspai.com/post/101028) | [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics) <br> [![Zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTk5QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics) |
**Chat Groups:** [QQ Group (1054700388)](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
</div>
## 🧪 Download & Install
<div align="center">
**_[中文版 README 请点按此处](https://github.com/jayfunc/BetterLyrics/blob/dev/README.CN.md)_**
| Microsoft Store (Recommended) | Manual Install |
| :---: | :---: |
| <a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" width="160"/></a><br>Unlimited free trial (Same as paid) | [**📦 Latest Release (.zip)**](https://github.com/jayfunc/BetterLyrics/releases/latest)<br>See [Installation Guide](https://jayfunc.blog/blog/how-to-install-zip) |
<mark>**_This project is under active development; unexpected issues may occur._**</mark>
[📖 User Guide](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide) | [🔒 Privacy Policy](PrivacyPolicy.md) | [⚖️ Terms of Service](TermsofService.md)
</div>
## ✍️ Help us translate into your language
## 🌟 Highlighted Features
Cannot find your language? Or have better translations? Don't worry! Start translating and becoming one of the contributors! 😆 See [here](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file) for more info on how to contribute.
- 🎨 **Stunning Visuals & UI**
- **Elegant Design:** Smooth, highly personalized style powered by WinUI3 & Win2D.
- **Immersive Effects:** Fluid backgrounds, 3D/Fan-shaped lyrics, snowflake particles, and more.
- **Deep Customization:** Configure animations, fonts, and behaviors to your taste.
## 🎉 This project was featured by SSPAI!
- 🎧 **Versatile Playback & Connectivity**
- **Built-in Player:** Play from **Local Drives** or stream via **Network Protocols** (SMB, WebDAV).
- **External Integration:** Visualizes music from Spotify, Apple Music, NetEase, and [many others](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide#known-supported-music-players-configuration-guide).
Check out the article: [BetterLyrics An immersive and smooth lyrics display tool designed for Windows](https://sspai.com/post/101028).
- 🌐 **Advanced Lyrics System**
- **Offline Translation:** Privacy-focused local machine translation (30+ languages).
- **Comprehensive Sources:** .lrc (Standard/Enhanced), .eslrc, .ttml, embedded tags, and online sources (QQ Music, NetEase, LRCLIB).
- **Apple Music:** Supports lyrics fetching (Requires token configuration).
## 🔈 Feedback and chat group
- 🪟 **Display Modes for Every Scenario**
- **Standard:** Full immersive experience.
- **Docked:** A sleek bar attached to your screen edge.
- **Desktop Overlay:** Lyrics floating above all apps.
[QQ 群](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388) | [Discord Server](https://discord.gg/5yAQPnyCKv) | [Telegram Group](https://t.me/+svhSLZ7awPsxNGY1)
## 🌟 Highlighted features
- 🌠 **Pleasing User Interface**
- Smooth and highly personalized style, animations and effects
- Immersive fluid background
- Perspective/fan-shaped lyrics
- Snowflake effect
- Multiple lyrics scrolling functions
- ... (and more)
- ↔️ **Strong Lyrics Translation**
- Offline machine translation (supporting 30+ languages)
- Auto-reading local lyrics files for embedded translation
- 🧩 **Various Lyrics Source**
- 💾 Local storage
- Music files (with embedded lyrics)
- [.lrc](<https://en.wikipedia.org/wiki/LRC_(file_format)>) files (with both core format and enhanced format)
- [.eslrc](https://github.com/ESLyric/release) files
- [.ttml](https://en.wikipedia.org/wiki/Timed_Text_Markup_Language) files
- ☁️ Online lyrics providers
- QQ 音乐
- 网易云音乐
- 酷狗音乐
- [amll-ttml-db](https://github.com/Steve-xmh/amll-ttml-db)
- [LRCLIB](https://lrclib.net/)
- <details><summary>⚠️ Apple Music (additional config needed)</summary>
- Open the Apple Music web app and the Developer Tools window. Refresh the page. Return to the Developer Tools window, select Fetch/XHR, select a request, find the Media-User-Token header in the request header, and copy its value.
- Open BetterLyrics and go to the Playback Source settings. Enter the copied value in the Media-User-Token (for Apple Music) setting and click the accept icon on the right-hand side.
- 🎶 **Multiple Music Players Supported**
- Check it out [here](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide#known-supported-music-players-configuration-guide) for detailed info
- 🪟 **Multiple Display Modes**
- **Standard Mode**
- Enjoy an immersive listening journey with rich lyrics, animations and beautifully dynamic backgrounds
- **Docked Mode**
- A smart animated lyrics bar docked to your screen edge
- **Desktop Mode**
- Enjoy immersive lyrics floating above your apps
- **And More...**
- Waiting for you to discover...
- 🧠 **Smart Behaviors**
- Auto hide when music paused
- Auto-hides when music pauses.
## 🖼️ Screenshots
![](Screenshots/fs2.png)
![](Screenshots/std.png)
![](Screenshots/narrow.png)
![](Screenshots/Snipaste_2025-10-31_19-23-17.png)
![](Screenshots/Snipaste_2025-10-31_19-27-34.png)
![](Screenshots/dock.png)
![](Screenshots/desktop.png)
<div align="center">
> ⚠️ Due to GIF format and frame rate limitations, the displayed effect is for preview only. Please refer to the actual device for the actual effect.
| Standard View | Narrow Mode |
| :---: | :---: |
| <img src="Screenshots/std.png" width="100%"> | <img src="Screenshots/narrow.png" width="100%"> |
![](Screenshots/PixPin_2025-10-24_18-13-44.gif)
![](Screenshots/PixPin_2025-10-24_18-17-17.gif)
| Lyrics Visual Effects | Coexisting Modes |
| :---: | :---: |
| <img src="Screenshots/effect.png" width="100%"> | <img src="Screenshots/all-in-one.png" width="100%"> |
| Fullscreen Mode | Fullscreen Mode |
| :---: | :---: |
| <img src="Screenshots/fs3.png" width="100%"> | <img src="Screenshots/fs2.png" width="100%"> |
| Music Gallery | Playback Statistics |
| :---: | :---: |
| <img src="Screenshots/music-gallery.png" width="100%"> | <img src="Screenshots/stats.png" width="100%"> |
</div>
## 📹 Demonstration
Watch our demo video (uploaded on 21 Oct 2025) on Bilibili [here](https://www.bilibili.com/video/BV1QRstz1EGt/).
> Watch our demo video (uploaded on 21 Oct 2025) on Bilibili [here](https://www.bilibili.com/video/BV1QRstz1EGt/).
## 🧪 Try it now
## ✍️ Contribute & Build
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
</a>
**Help us translate:** Cannot find your language? [Start translating here](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file).
**Unlimited** free trail or purchase (there is **no difference** between free and paid version).
If you find it useful, please consider [donating](#-donations) or purchasing it in **Microsoft Store**, I'll appreciate it! 🥰
Having trouble downloading and installing from the MS Store? Try the following options:
- [Download from outside Microsoft Store](https://jayfunc.blog/blog/download-from-outside-ms-store)
- Go to [latest release](https://github.com/jayfunc/BetterLyrics/releases/latest) and download `.zip` file from `Assets`. (See [this doc](https://jayfunc.blog/blog/how-to-install-zip) for how to install it.)
## 🏗️ Build
Before you build, make sure that you have already:
- Replaced `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate` with `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate.cs`.
- Replaced `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFMTemplate` with `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFM.cs`.
**Build from source:**
> Before building, ensure you have replaced `DiscordTemplate.cs` and `LastFM.cs` in the `Constants` folder.
## 🤑 Donations
If you like this project, please consider supporting it by donating. Your support will help keep the project alive and encourage further development.
If you like BetterLyrics, please consider supporting it. Your support helps keep the project alive!
You can donate via:
- [PayPal](https://paypal.me/zhefangpay)
- [Buy Me a Coffee](https://buymeacoffee.com/founchoo)
- [爱发电](https://afdian.com/a/jayfunc)
- <details><summary>支付宝</summary>
![](Donate/Alipay.jpg)
</detais>
<div align="center">
- <details><summary>微信</summary>
![](Donate/WeChatReward.png)
</details>
| Web Platforms | Alipay (QR) | WeChat (QR) |
| :---: | :---: | :---: |
| [PayPal](https://paypal.me/zhefangpay)<br><br>[Buy Me a Coffee](https://buymeacoffee.com/founchoo)<br><br>[爱发电 (Afdian)](https://afdian.com/a/jayfunc) | <img src="Donate/Alipay.jpg" width="150"> | <img src="Donate/WeChatReward.png" width="150"> |
This project is made possible by the generous support of our users. **[View the full Hall of Fame](SPONSORS.md)**
**[View the full Hall of Fame (Sponsors)](SPONSORS.md)**
## 📄 License
</div>
This project is licensed under the GNU General Public License v3.0. See the [LICENSE](https://github.com/jayfunc/BetterLyrics/blob/dev/LICENSE) file for details.
## ⭐ Star History
## 💖 Many thanks to
<div align="center">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date" width="100%">
</div>
Some functions and code are referenced or modified from public repositories, including but not limited to the following open source projects/packages, tutorials, etc., and we would like to express our gratitude to them here.
## 📄 License & Credits
This project is licensed under the **GNU General Public License v3.0**.
<details>
<summary><b>💖 Special Thanks, Credits & Inspiration</b></summary>
<br>
**Dependencies & References:**
| Projects/Packages | Description |
| :--- | :--- |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | Provide lyrics fetch, decryption, and parsing for QQ, Netease, and Kugou sources |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | Lyrics fetch/decrypt for QQ, Netease, Kugou |
| [lrclib](https://github.com/tranxuanthang/lrclib) | LRCLIB lyrics API provider |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | Apple Music lyrics fetch using Python |
| [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet) | Used for extracting pictures from music files |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | Provide easy ways to access the Win32 API regarding windowing |
| [TagLib#](https://github.com/mono/taglib-sharp) | Used for reading the original lyrics content |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | Apple Music lyrics fetch |
| [Audio Tools Library (ATL)](https://github.com/Zeugma440/atldotnet) | Picture extraction from music files |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | Win32 API windowing access |
| [TagLib#](https://github.com/mono/taglib-sharp) | Reading original lyrics content |
| [Vanara](https://github.com/dahall/Vanara) | Win32 API wrapper |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | Provide the ability for offline lyrics translation |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | Dynamic fluid background implementation |
| [SpectrumVisualization](https://github.com/Johnwikix/SpectrumVisualization) | Audio visualization reference |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | Provide many out-of-the-box features for building WinUI 3 applications |
| ... | ... |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | Offline lyrics translation |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | Dynamic fluid background |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | WinUI 3 helpers |
See all the dependencies [here](https://github.com/jayfunc/BetterLyrics/network/dependencies).
### Tutorials/Blogs/etc.
See [dependencies](https://github.com/jayfunc/BetterLyrics/network/dependencies) for full list.
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
- [Bilibili -【WinUI3】SystemBackdropController定义云母、亚克力效果](https://www.bilibili.com/video/BV1PY4FevEkS)
- [cnblogs - .NET App 与 Windows 系统媒体控制(SMTC)交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 中的游戏循环CanvasAnimatedControl](https://www.cnblogs.com/walterlv/p/10236395.html)
- [r2d2rigo/Win2D-Samples](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
- [CommunityToolkit - 从入门到精通](https://mvvm.coldwind.top/)
<br>
## 💡 Inspired by
Some design ideas are referenced from the following plugins/software (excluding code that is indirectly or directly referenced or modified, and is only used as a guide for design ideas).
**💡 Inspired by:**
Some design ideas are referenced from the following projects (design inspiration only):
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
## ⭐ Star history
</details>
<div style="display: flex; justify-content: space-around; align-items: flex-start;">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date" width="100%" >
## 💭 Share on Social Media
<details>
<summary><b>Click to expand</b></summary>
<br>
<div align="center">
<img src="https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light" width="48%">
<img src="https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics" width="48%">
</div>
</details>
## 🤗 Any issues and PRs are welcome
<br>
If you find a bug, please file it in issues, or if you have any ideas, feel free to share them here.
## ⚠️ Disclaimer
This project is provided "as is" without warranty of any kind.
All lyrics, fonts, icons, and other third-party resources are the property of their respective copyright holders.
The author of this project does not claim ownership of such resources.
This project is non-commercial and should not be used to infringe any rights.
Users are responsible for ensuring their own use complies with applicable laws and licenses.
## 💭 Share it on social media
![BetterLyrics](https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light)
![BetterLyrics](https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics)
<div align="center">
<mark><i>This project is under active development; unexpected issues may occur.</i></mark><br>
<sub>Disclaimer: This project is provided "as is". All third-party resources belong to their respective owners.</sub>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

BIN
Screenshots/all-in-one.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

BIN
Screenshots/effect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
Screenshots/fs3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

BIN
Screenshots/stats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB