From 6ca2d1f89704dbac139a5defba5fd921a2323b73 Mon Sep 17 00:00:00 2001 From: Zhe Fang Date: Tue, 30 Dec 2025 19:49:54 -0500 Subject: [PATCH] chores --- .../Package.appxmanifest | 2 +- .../BetterLyrics.WinUI3/App.xaml.cs | 203 +++++++++++-- .../BetterLyrics.WinUI3.csproj | 10 +- .../Controls/AboutControl.xaml | 6 + .../BetterLyrics.WinUI3/Helper/FileHelper.cs | 13 + .../BetterLyrics.WinUI3/Helper/PathHelper.cs | 2 +- .../Models/Db/FilesIndexDbContext.cs | 14 + .../Models/Db/PlayHistoryDbContext.cs | 14 + .../Models/ExtendedTrack.cs | 2 +- .../Models/FileCacheEntity.cs | 53 ---- .../Models/FilesIndexItem.cs | 67 +++++ .../Models/PlayHistoryItem.cs | 39 ++- .../AlbumArtSearchService.cs | 2 +- .../FileSystemService/FileSystemService.cs | 281 ++++++++---------- .../FileSystemService/IFileSystemService.cs | 14 +- .../FileSystemService/IUnifiedFileSystem.cs | 4 +- .../Providers/FTPFileSystem.cs | 22 +- .../Providers/LocalFileSystem.cs | 18 +- .../Providers/SMBFileSystem.cs | 41 ++- .../Providers/WebDavFileSystem.cs | 21 +- .../LyricsSearchService.cs | 4 +- .../PlayHistoryService/IPlayHistoryService.cs | 1 - .../PlayHistoryService/PlayHistoryService.cs | 239 +++++++-------- .../Strings/ar/Resources.resw | 41 +-- .../Strings/de/Resources.resw | 41 +-- .../Strings/en/Resources.resw | 15 +- .../Strings/es/Resources.resw | 41 +-- .../Strings/fr/Resources.resw | 41 +-- .../Strings/hi/Resources.resw | 41 +-- .../Strings/id/Resources.resw | 41 +-- .../Strings/ja/Resources.resw | 41 +-- .../Strings/ko/Resources.resw | 41 +-- .../Strings/ms/Resources.resw | 41 +-- .../Strings/pt/Resources.resw | 41 +-- .../Strings/ru/Resources.resw | 41 +-- .../Strings/th/Resources.resw | 41 +-- .../Strings/vi/Resources.resw | 41 +-- .../Strings/zh-Hans/Resources.resw | 46 +-- .../Strings/zh-Hant/Resources.resw | 41 +-- .../ViewModels/AboutControlViewModel.cs | 14 + .../ViewModels/MusicGalleryPageViewModel.cs | 13 +- .../Views/MusicGalleryPage.xaml | 15 +- 42 files changed, 996 insertions(+), 753 deletions(-) create mode 100644 BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Db/FilesIndexDbContext.cs create mode 100644 BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Db/PlayHistoryDbContext.cs delete mode 100644 BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FileCacheEntity.cs create mode 100644 BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/FilesIndexItem.cs diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest index e2120c0..d7a2ee3 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest @@ -12,7 +12,7 @@ + Version="1.2.231.0" /> diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml.cs index 9f94d77..e1f5afc 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml.cs +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml.cs @@ -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; @@ -17,36 +15,42 @@ 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 _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(); @@ -54,28 +58,81 @@ namespace BetterLyrics.WinUI3 _logger = Ioc.Default.GetRequiredService>(); + // 注册全局异常捕获 UnhandledException += App_UnhandledException; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; } - private void EnsureSingleInstance() + /// + /// 处理单实例逻辑。 + /// 返回 true 表示我是主实例,继续运行。 + /// 返回 false 表示我是第二个实例,已通知主实例,我应该退出。 + /// + 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) + /// + /// 当第二个实例试图启动时,主实例会收到此回调 + /// + private void OnMainInstanceActivated(object? sender, AppActivationArguments e) { - var settingsService = Ioc.Default.GetRequiredService(); + // 这个事件是在后台线程触发的,必须切回 UI 线程操作窗口 + m_window?.DispatcherQueue.TryEnqueue(() => + { + HandleActivation(); + }); + } + /// + /// 唤醒逻辑 + /// + private void HandleActivation() + { + WindowHook.OpenOrShowWindow(); + } + + protected override async void OnLaunched(LaunchActivatedEventArgs args) + { + // 初始化数据库 + await EnsureDatabasesAsync(); + + var settingsService = Ioc.Default.GetRequiredService(); var fileSystemService = Ioc.Default.GetRequiredService(); + + // 开始后台扫描任务 foreach (var item in settingsService.AppSettings.LocalMediaFolders) { if (item.LastSyncTime == null) @@ -85,8 +142,10 @@ namespace BetterLyrics.WinUI3 } fileSystemService.StartAllFolderTimers(); - WindowHook.OpenOrShowWindow(); + // 初始化托盘 + m_window = WindowHook.OpenOrShowWindow(); + // 根据设置打开歌词窗口 if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow) { var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault); @@ -102,12 +161,101 @@ namespace BetterLyrics.WinUI3 } } } + + // 根据设置自动打开主界面 if (settingsService.AppSettings.MusicGallerySettings.AutoOpen) { WindowHook.OpenOrShowWindow(); } } + private async Task EnsureDatabasesAsync() + { + var playHistoryFactory = Ioc.Default.GetRequiredService>(); + var fileCacheFactory = Ioc.Default.GetRequiredService>(); + + 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 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() @@ -115,14 +263,19 @@ namespace BetterLyrics.WinUI3 .WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day) .CreateLogger(); - // Register services Ioc.Default.ConfigureServices( new ServiceCollection() + // 数据库工厂 + .AddDbContextFactory(options => options.UseSqlite($"Data Source={PathHelper.PlayHistoryPath}")) + .AddDbContextFactory(options => options.UseSqlite($"Data Source={PathHelper.FilesIndexPath}")) + + // 日志 .AddLogging(loggingBuilder => { loggingBuilder.ClearProviders(); loggingBuilder.AddSerilog(); }) + // Services .AddSingleton() .AddSingleton() @@ -135,6 +288,7 @@ namespace BetterLyrics.WinUI3 .AddSingleton() .AddSingleton() .AddSingleton() + // ViewModels .AddSingleton() .AddSingleton() @@ -167,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) @@ -180,4 +335,4 @@ namespace BetterLyrics.WinUI3 _logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException"); } } -} +} \ No newline at end of file diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj index 12b1792..8dfb956 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj @@ -87,6 +87,11 @@ + + + + + @@ -97,7 +102,6 @@ - @@ -130,6 +134,10 @@ + + + + diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/AboutControl.xaml b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/AboutControl.xaml index b55d489..7139e53 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/AboutControl.xaml +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Controls/AboutControl.xaml @@ -190,6 +190,12 @@ + + +