Files
LanMountainDesktop/LanMountainDesktop/App.axaml.cs

1330 lines
45 KiB
C#
Raw Normal View History

2026-03-12 21:01:23 +08:00
using System;
2026-03-13 22:20:12 +08:00
using System.Globalization;
2026-03-12 21:01:23 +08:00
using System.Linq;
2026-03-13 22:20:12 +08:00
using System.Threading;
2026-03-14 22:45:09 +08:00
using System.Threading.Tasks;
2026-03-10 16:35:43 +08:00
using Avalonia;
using Avalonia.Controls;
2026-02-26 23:08:19 +08:00
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
2026-03-14 13:36:18 +08:00
using Avalonia.Media;
2026-03-10 16:35:43 +08:00
using Avalonia.Platform;
2026-03-13 22:20:12 +08:00
using Avalonia.Styling;
2026-03-08 14:00:13 +08:00
using Avalonia.Threading;
2026-03-12 21:01:23 +08:00
using AvaloniaWebView;
using LanMountainDesktop.ComponentSystem;
2026-03-20 00:41:14 +08:00
using LanMountainDesktop.DesktopHost;
2026-03-13 09:10:00 +08:00
using LanMountainDesktop.Models;
2026-03-12 21:01:23 +08:00
using LanMountainDesktop.PluginSdk;
2026-03-05 12:34:39 +08:00
using LanMountainDesktop.Services;
2026-04-16 19:28:58 +08:00
using LanMountainDesktop.Services.Launcher;
using LanMountainDesktop.Services.Loading;
2026-03-13 09:10:00 +08:00
using LanMountainDesktop.Services.Settings;
2026-04-16 19:28:58 +08:00
using LanMountainDesktop.Shared.Contracts.Launcher;
2026-03-14 13:36:18 +08:00
using LanMountainDesktop.Theme;
2026-03-04 15:22:52 +08:00
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views;
2026-02-26 23:08:19 +08:00
2026-03-04 15:22:52 +08:00
namespace LanMountainDesktop;
2026-02-26 23:08:19 +08:00
public partial class App : Application
{
2026-03-14 13:36:18 +08:00
private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6");
2026-03-12 21:01:23 +08:00
private enum DesktopShellState
{
ForegroundDesktop = 0,
MinimizedToTaskbar = 1,
TrayOnly = 2
}
private enum ShutdownIntent
{
None = 0,
ExitRequested = 1,
RestartRequested = 2
}
2026-03-13 09:10:00 +08:00
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
2026-03-15 17:08:07 +08:00
private readonly IAppearanceThemeService _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
private readonly IAppLogoService _appLogoService = HostAppLogoProvider.GetOrCreate();
2026-03-10 16:35:43 +08:00
private readonly LocalizationService _localizationService = new();
2026-03-22 23:30:43 +08:00
private readonly FontFamilyService _fontFamilyService = new();
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
2026-03-13 22:20:12 +08:00
private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService();
2026-03-14 22:45:09 +08:00
private readonly ILocationService _locationService = HostLocationServiceProvider.GetOrCreate();
2026-03-13 22:20:12 +08:00
private ISettingsPageRegistry? _settingsPageRegistry;
private ISettingsWindowService? _settingsWindowService;
2026-03-14 22:45:09 +08:00
private WeatherLocationRefreshService? _weatherLocationRefreshService;
private INotificationService? _notificationService;
private bool _exitCleanupCompleted;
2026-03-12 21:01:23 +08:00
private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop;
private ShutdownIntent _shutdownIntent;
2026-03-10 16:35:43 +08:00
2026-03-21 16:16:02 +08:00
private TrayIcon? _trayIcon;
private NativeMenuItem? _trayShowDesktopMenuItem;
private NativeMenuItem? _traySettingsMenuItem;
private NativeMenuItem? _trayComponentLibraryMenuItem;
private NativeMenuItem? _trayRestartMenuItem;
private NativeMenuItem? _trayExitMenuItem;
2026-03-09 12:27:33 +08:00
private PluginRuntimeService? _pluginRuntimeService;
2026-03-12 21:01:23 +08:00
private MainWindow? _mainWindow;
private TransparentOverlayWindow? _transparentOverlayWindow;
2026-03-12 21:01:23 +08:00
private bool _mainWindowClosed;
2026-03-13 22:20:12 +08:00
private bool _uiUnhandledExceptionHooked;
2026-03-20 00:41:14 +08:00
private DesktopShellHost? _desktopShellHost;
2026-04-16 19:28:58 +08:00
private LauncherIpcClient? _launcherIpcClient;
private LoadingStateManager? _loadingStateManager;
private LoadingStateReporter? _loadingStateReporter;
private bool _singleInstanceReleased;
private int _forcedExitScheduled;
2026-03-09 12:27:33 +08:00
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
(Current as App)?._hostApplicationLifecycle;
internal static INotificationService? CurrentNotificationService =>
(Current as App)?._notificationService;
// 隐私政策查看事件
public static event Action? CurrentPrivacyPolicyViewRequested;
// 触发隐私政策查看事件的方法
public static void RaisePrivacyPolicyViewRequested()
{
CurrentPrivacyPolicyViewRequested?.Invoke();
}
2026-03-09 12:27:33 +08:00
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
2026-03-13 09:10:00 +08:00
public ISettingsFacadeService SettingsFacade => _settingsFacade;
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
2026-03-13 22:20:12 +08:00
internal ISettingsWindowService? SettingsWindowService => _settingsWindowService;
internal INotificationService? NotificationService => _notificationService;
2026-03-08 14:00:13 +08:00
2026-03-12 21:01:23 +08:00
internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
{
2026-03-13 22:20:12 +08:00
EnsureSettingsWindowService();
2026-03-13 00:33:00 +08:00
AppLogger.Info(
"SettingsFacade",
2026-03-13 22:20:12 +08:00
$"Opening settings window. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
_settingsWindowService?.Open(new SettingsWindowOpenRequest(
Source: source,
Owner: _mainWindow is { IsVisible: true } ? _mainWindow : null,
PageId: pageTag));
2026-03-12 21:01:23 +08:00
}
2026-03-14 13:36:18 +08:00
public App()
{
2026-03-22 02:53:31 +08:00
if (Design.IsDesignMode)
{
return;
}
2026-03-14 13:36:18 +08:00
_settingsFacade.Settings.Changed += OnSettingsChanged;
2026-03-15 17:08:07 +08:00
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
2026-03-14 13:36:18 +08:00
}
2026-02-26 23:08:19 +08:00
public override void Initialize()
{
2026-03-10 21:25:47 +08:00
AppLogger.Info("App", "Initializing application resources.");
2026-03-22 02:53:31 +08:00
AvaloniaXamlLoader.Load(this);
if (Design.IsDesignMode)
{
ApplyDesignTimeTheme();
return;
}
2026-03-05 12:34:39 +08:00
ConfigureWebViewUserDataFolder();
AvaloniaWebViewBuilder.Initialize(default);
2026-03-14 13:36:18 +08:00
ApplyThemeFromSettings();
2026-03-13 22:20:12 +08:00
ApplyCurrentCultureFromSettings();
EnsureSettingsWindowService();
2026-03-14 22:45:09 +08:00
EnsureWeatherLocationRefreshService();
EnsureNotificationService();
2026-02-26 23:08:19 +08:00
}
public override void OnFrameworkInitializationCompleted()
2026-02-26 23:08:19 +08:00
{
2026-03-22 02:53:31 +08:00
if (Design.IsDesignMode)
{
base.OnFrameworkInitializationCompleted();
return;
}
2026-03-10 21:25:47 +08:00
AppLogger.Info("App", "Framework initialization completed.");
2026-04-16 19:28:58 +08:00
2026-03-13 22:20:12 +08:00
RegisterUiUnhandledExceptionGuard();
2026-03-07 00:58:52 +08:00
LinuxDesktopEntryInstaller.EnsureInstalled();
2026-03-20 00:41:14 +08:00
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
2026-03-07 00:58:52 +08:00
2026-04-03 11:42:00 +08:00
if (!Design.IsDesignMode && OperatingSystem.IsWindows())
{
FusedDesktopManagerServiceFactory.GetOrCreate().Initialize();
}
2026-03-20 00:41:14 +08:00
base.OnFrameworkInitializationCompleted();
// IPC 初始化移到窗口创建之后,避免 async void 中的 await 导致窗口创建延迟
// 使用 fire-and-forget 模式,不阻塞主流程
_ = InitializeLauncherIpcAsync();
2026-03-20 00:41:14 +08:00
}
2026-04-16 19:28:58 +08:00
private async Task InitializeLauncherIpcAsync()
{
if (!LauncherIpcClient.IsLaunchedByLauncher())
return;
try
{
_launcherIpcClient = new LauncherIpcClient();
var connected = await _launcherIpcClient.ConnectAsync();
if (connected)
{
AppLogger.Info("LauncherIpc", "Connected to Launcher IPC server.");
// 初始化加载状态管理器
_loadingStateManager = new LoadingStateManager();
_loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _launcherIpcClient);
_loadingStateReporter.Start();
// 注册系统初始化加载项
_loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "系统初始化", "初始化系统核心组件");
_loadingStateManager.StartItem("system.init", "已连接启动器");
2026-04-17 15:16:01 +08:00
ReportStartupProgress(StartupStage.Initializing, 10, "正在初始化...");
ReportStartupProgress(StartupStage.LoadingSettings, 20, "正在加载设置...");
2026-04-16 19:28:58 +08:00
}
}
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to initialize Launcher IPC: {ex.Message}");
}
}
2026-03-20 00:41:14 +08:00
2026-04-17 15:16:01 +08:00
/// <summary>
/// 向 Launcher 报告启动进度fire-and-forget不阻塞主流程
/// </summary>
private void ReportStartupProgress(StartupStage stage, int percent, string message)
{
if (_launcherIpcClient is null)
return;
_ = Task.Run(async () =>
{
try
{
await _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage
{
Stage = stage,
ProgressPercent = percent,
Message = message
});
}
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}");
}
});
}
/// <summary>
/// 向 Launcher 报告关键启动进度,使用后台线程避免阻塞 UI
/// 用于 Ready 等关键状态报告
/// </summary>
private void ReportStartupProgressSync(StartupStage stage, int percent, string message)
{
if (_launcherIpcClient is null)
return;
try
{
_ = Task.Run(async () =>
{
try
{
await _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage
{
Stage = stage,
ProgressPercent = percent,
Message = message
});
AppLogger.Info("LauncherIpc", $"Successfully reported stage: {stage}");
}
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to report progress: {ex.Message}");
}
});
}
catch (Exception ex)
{
AppLogger.Warn("LauncherIpc", $"Failed to launch progress report task: {ex.Message}");
}
}
2026-03-22 02:53:31 +08:00
private void ApplyDesignTimeTheme()
{
RequestedThemeVariant = ThemeVariant.Light;
try
{
ApplyAdaptiveThemeResources();
}
catch (Exception ex)
{
AppLogger.Warn("Previewer", "Failed to apply adaptive theme resources in design mode.", ex);
}
}
2026-03-20 00:41:14 +08:00
private void InitializeDesktopShell()
{
_desktopShellHost ??= new DesktopShellHost(
InitializePluginRuntime,
InitializeTrayIcon,
desktop =>
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown;
2026-04-17 15:16:01 +08:00
ReportStartupProgress(StartupStage.InitializingUI, 60, "正在初始化界面...");
2026-03-20 00:41:14 +08:00
CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
},
OnDesktopLifetimeExit,
2026-03-20 00:41:14 +08:00
() => CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow),
StartWeatherLocationRefreshIfNeeded);
_desktopShellHost.Initialize(this);
2026-02-26 23:08:19 +08:00
}
private void OnDesktopLifetimeExit()
{
AppLogger.Info("App", "Desktop lifetime exit triggered.");
PerformExitCleanup();
ReleaseSingleInstanceAfterExit("DesktopLifetimeExit");
ScheduleForcedProcessTermination("DesktopLifetimeExit");
}
private void OnTrayExitClick(object? sender, EventArgs e)
{
_ = _hostApplicationLifecycle.TryExit(new HostApplicationLifecycleRequest(
Source: "TrayMenu",
Reason: "User selected Exit App from the tray menu."));
}
2026-03-12 21:01:23 +08:00
private void OnTrayShowDesktopClick(object? sender, EventArgs e)
2026-03-08 14:00:13 +08:00
{
2026-03-12 21:01:23 +08:00
RestoreOrCreateMainWindow(showSingleInstanceNotice: false, source: "TrayMenu");
}
2026-03-08 14:00:13 +08:00
private void OnTrayRestartClick(object? sender, EventArgs e)
{
_ = _hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest(
Source: "TrayMenu",
Reason: "User selected Restart App from the tray menu."));
2026-03-10 16:35:43 +08:00
}
2026-03-13 22:20:12 +08:00
private void OnTraySettingsClick(object? sender, EventArgs e)
{
_ = sender;
_ = e;
OpenIndependentSettingsModule("TrayMenu");
}
private void OnTrayComponentLibraryClick(object? sender, EventArgs e)
{
_ = sender;
_ = e;
// 仅在 Windows 上支持融合桌面功能
if (!OperatingSystem.IsWindows())
2026-03-13 22:20:12 +08:00
{
AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows.");
2026-03-13 22:20:12 +08:00
return;
}
2026-04-03 11:42:00 +08:00
// 切换进入编辑模式,隐藏常态零散的小部件
FusedDesktopManagerServiceFactory.GetOrCreate().EnterEditMode();
// 确保透明覆盖层窗口存在并显示
EnsureTransparentOverlayWindow();
// 打开融合桌面组件库窗口
Dispatcher.UIThread.Post(() =>
{
try
{
// 确保覆盖层窗口已显示(组件要渲染在上面,必须先 Show
if (_transparentOverlayWindow is not null && !_transparentOverlayWindow.IsVisible)
{
_transparentOverlayWindow.Show();
}
var window = new FusedDesktopComponentLibraryWindow();
if (_transparentOverlayWindow is not null)
{
window.SetOverlayWindow(_transparentOverlayWindow);
}
2026-04-03 11:42:00 +08:00
// 当组件库关闭时,退出编辑态
window.Closed += (s, ev) =>
{
if (_transparentOverlayWindow is not null)
{
// 触发画布保存,并隐藏画布
_transparentOverlayWindow.SaveLayoutAndHide();
}
// 让管理器根据已存储的最新快照重建生成所有实体小组件
FusedDesktopManagerServiceFactory.GetOrCreate().ExitEditMode();
};
window.Show();
window.Activate();
}
catch (Exception ex)
{
AppLogger.Warn("FusedDesktop", "Failed to open fused desktop component library.", ex);
}
}, DispatcherPriority.Send);
2026-03-13 22:20:12 +08:00
}
2026-02-26 23:08:19 +08:00
private void DisableAvaloniaDataAnnotationValidation()
{
// Get an array of plugins to remove
var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
// remove each entry found
foreach (var plugin in dataValidationPluginsToRemove)
{
BindingPlugins.DataValidators.Remove(plugin);
}
}
2026-03-05 12:34:39 +08:00
private static void ConfigureWebViewUserDataFolder()
{
if (!OperatingSystem.IsWindows())
{
return;
}
const string userDataFolderEnvVar = "WEBVIEW2_USER_DATA_FOLDER";
try
{
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(userDataFolderEnvVar)))
{
return;
}
var userDataFolder = WebView2RuntimeProbe.ResolveUserDataFolder();
Environment.SetEnvironmentVariable(
userDataFolderEnvVar,
userDataFolder,
EnvironmentVariableTarget.Process);
}
2026-03-10 21:25:47 +08:00
catch (Exception ex)
2026-03-05 12:34:39 +08:00
{
// Keep startup resilient if user profile folders are unavailable.
2026-03-10 21:25:47 +08:00
AppLogger.Warn("WebView2", "Failed to configure WebView2 user data folder.", ex);
2026-03-05 12:34:39 +08:00
}
}
2026-03-09 12:27:33 +08:00
private void InitializePluginRuntime()
{
2026-04-17 15:16:01 +08:00
ReportStartupProgress(StartupStage.LoadingPlugins, 30, "正在加载插件...");
2026-03-09 12:27:33 +08:00
try
{
_pluginRuntimeService?.Dispose();
2026-03-13 09:10:00 +08:00
_pluginRuntimeService = new PluginRuntimeService(_settingsFacade);
HostSettingsFacadeProvider.BindPluginRuntime(_pluginRuntimeService);
2026-03-09 12:27:33 +08:00
_pluginRuntimeService.LoadInstalledPlugins();
}
catch (Exception ex)
{
2026-03-10 21:25:47 +08:00
AppLogger.Warn("PluginRuntime", "Failed to initialize plugin runtime.", ex);
2026-03-09 12:27:33 +08:00
}
}
2026-03-10 16:35:43 +08:00
private void InitializeTrayIcon()
{
try
{
2026-03-21 16:16:02 +08:00
if (_trayIcon is null)
2026-03-10 16:35:43 +08:00
{
2026-03-21 16:16:02 +08:00
_trayShowDesktopMenuItem = new NativeMenuItem();
_trayShowDesktopMenuItem.Click += OnTrayShowDesktopClick;
_traySettingsMenuItem = new NativeMenuItem();
_traySettingsMenuItem.Click += OnTraySettingsClick;
_trayComponentLibraryMenuItem = new NativeMenuItem();
_trayComponentLibraryMenuItem.Click += OnTrayComponentLibraryClick;
_trayRestartMenuItem = new NativeMenuItem();
_trayRestartMenuItem.Click += OnTrayRestartClick;
_trayExitMenuItem = new NativeMenuItem();
_trayExitMenuItem.Click += OnTrayExitClick;
var trayMenu = new NativeMenu();
trayMenu.Items.Add(_trayShowDesktopMenuItem);
trayMenu.Items.Add(_traySettingsMenuItem);
trayMenu.Items.Add(_trayComponentLibraryMenuItem);
trayMenu.Items.Add(new NativeMenuItemSeparator());
trayMenu.Items.Add(_trayRestartMenuItem);
trayMenu.Items.Add(new NativeMenuItemSeparator());
trayMenu.Items.Add(_trayExitMenuItem);
2026-03-10 16:35:43 +08:00
2026-03-21 16:16:02 +08:00
_trayIcon = new TrayIcon
{
Icon = _appLogoService.CreateTrayIcon(),
Menu = trayMenu,
IsVisible = true
};
TrayIcon.SetIcons(this, [_trayIcon]);
}
RefreshTrayIconContent();
2026-03-10 16:35:43 +08:00
}
catch (Exception ex)
{
2026-03-10 21:25:47 +08:00
AppLogger.Warn("TrayIcon", "Failed to initialize tray icon.", ex);
2026-03-10 16:35:43 +08:00
}
}
2026-03-21 16:16:02 +08:00
private void RefreshTrayIconContent()
2026-03-10 16:35:43 +08:00
{
2026-03-21 16:16:02 +08:00
if (_trayIcon is not null)
{
_trayIcon.IsVisible = true;
if (!OperatingSystem.IsLinux())
{
_trayIcon.ToolTipText = L("tray.tooltip", "LanMountainDesktop");
}
}
2026-03-13 22:20:12 +08:00
2026-03-21 16:16:02 +08:00
if (_trayShowDesktopMenuItem is not null)
{
_trayShowDesktopMenuItem.Header = L("tray.menu.show_desktop", "Open Desktop");
}
2026-03-12 21:01:23 +08:00
2026-03-21 16:16:02 +08:00
if (_traySettingsMenuItem is not null)
{
_traySettingsMenuItem.Header = L("tray.menu.settings", "Settings");
}
2026-03-10 16:35:43 +08:00
RefreshFusedDesktopMenuItemVisibility();
2026-03-10 16:35:43 +08:00
2026-03-21 16:16:02 +08:00
if (_trayRestartMenuItem is not null)
{
_trayRestartMenuItem.Header = L("tray.menu.restart", "Restart App");
}
2026-03-10 16:35:43 +08:00
2026-03-21 16:16:02 +08:00
if (_trayExitMenuItem is not null)
{
_trayExitMenuItem.Header = L("tray.menu.exit", "Exit App");
}
2026-03-10 16:35:43 +08:00
}
private void RefreshFusedDesktopMenuItemVisibility()
{
if (_trayComponentLibraryMenuItem is null)
{
return;
}
// 仅在 Windows 上支持融合桌面功能
if (!OperatingSystem.IsWindows())
{
_trayComponentLibraryMenuItem.IsVisible = false;
return;
}
// 检查融合桌面功能是否启用
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
_trayComponentLibraryMenuItem.IsVisible = appSnapshot.EnableFusedDesktop;
if (_trayComponentLibraryMenuItem.IsVisible)
{
_trayComponentLibraryMenuItem.Header = L("tray.menu.component_library", "Component Library");
}
}
2026-03-10 16:35:43 +08:00
private void DisposeTrayIcon()
{
2026-03-21 16:16:02 +08:00
if (_trayIcon is null)
2026-03-10 16:35:43 +08:00
{
return;
}
2026-03-21 16:16:02 +08:00
try
2026-03-10 16:35:43 +08:00
{
2026-03-21 16:16:02 +08:00
_trayIcon.IsVisible = false;
}
catch (Exception ex)
{
AppLogger.Warn("TrayIcon", "Failed to hide tray icon during cleanup.", ex);
2026-03-10 16:35:43 +08:00
}
}
2026-03-13 22:20:12 +08:00
private void EnsureSettingsWindowService()
{
_settingsPageRegistry ??= new SettingsPageRegistry(
_settingsFacade,
_hostApplicationLifecycle,
_localizationService,
() => _pluginRuntimeService);
_settingsWindowService ??= new SettingsWindowService(
_settingsPageRegistry,
_hostApplicationLifecycle,
_settingsFacade);
}
2026-03-14 22:45:09 +08:00
private void EnsureWeatherLocationRefreshService()
{
_weatherLocationRefreshService ??= new WeatherLocationRefreshService(
_settingsFacade,
_locationService,
_localizationService);
}
private void EnsureNotificationService()
{
_notificationService ??= new NotificationService(_appearanceThemeService);
}
2026-03-14 22:45:09 +08:00
private void StartWeatherLocationRefreshIfNeeded()
{
EnsureWeatherLocationRefreshService();
if (_weatherLocationRefreshService is null)
{
return;
}
_ = Task.Run(async () =>
{
try
{
await _weatherLocationRefreshService.TryRefreshOnStartupAsync();
}
catch (Exception ex)
{
AppLogger.Warn("Weather.Location", "Failed to refresh weather location during startup.", ex);
}
});
}
2026-03-14 13:36:18 +08:00
private void ApplyThemeFromSettings()
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
var snapshot = _appearanceThemeService.GetCurrent();
RequestedThemeVariant = snapshot.IsNightMode
2026-03-13 22:20:12 +08:00
? ThemeVariant.Dark
: ThemeVariant.Light;
2026-03-15 17:08:07 +08:00
ApplyAdaptiveThemeResources();
2026-03-13 22:20:12 +08:00
}
private void ApplyCurrentCultureFromSettings()
{
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
CultureInfo culture;
try
{
culture = CultureInfo.GetCultureInfo(languageCode);
}
catch (CultureNotFoundException)
{
culture = CultureInfo.GetCultureInfo("zh-CN");
}
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
2026-03-22 23:30:43 +08:00
ApplyLanguageSpecificFont(languageCode);
}
private void ApplyLanguageSpecificFont(string languageCode)
{
var fontFamily = _fontFamilyService.GetFontFamilyForLanguage(languageCode);
if (Resources.TryGetValue("AppFontFamily", out var currentFont) &&
currentFont is FontFamily currentFontFamily &&
currentFontFamily.Name == fontFamily.Name)
{
return;
}
Resources["AppFontFamily"] = fontFamily;
2026-03-13 22:20:12 +08:00
}
private void ActivateMainWindow()
2026-03-12 21:01:23 +08:00
{
AppLogger.Info("SingleInstance", $"Activation callback received. Pid={Environment.ProcessId}.");
try
{
var restored = Dispatcher.UIThread.CheckAccess()
? RestoreOrCreateMainWindowCore(showSingleInstanceNotice: true, source: "SingleInstance")
: Dispatcher.UIThread.InvokeAsync(
() => RestoreOrCreateMainWindowCore(showSingleInstanceNotice: true, source: "SingleInstance"),
DispatcherPriority.Send).GetAwaiter().GetResult();
if (!restored)
{
throw new InvalidOperationException("Main window restore failed in activation callback.");
}
AppLogger.Info("SingleInstance", "Activation callback completed successfully.");
}
catch (Exception ex)
{
AppLogger.Warn("SingleInstance", "Activation callback failed while restoring the desktop shell.", ex);
throw;
}
2026-03-12 21:01:23 +08:00
}
private void RestoreOrCreateMainWindow(bool showSingleInstanceNotice, string source)
{
Dispatcher.UIThread.Post(() =>
{
_ = RestoreOrCreateMainWindowCore(showSingleInstanceNotice, source);
}, DispatcherPriority.Send);
}
private bool RestoreOrCreateMainWindowCore(bool showSingleInstanceNotice, string source)
{
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
{
AppLogger.Warn("DesktopShell", $"Restore skipped because desktop lifetime is unavailable. Source='{source}'.");
return false;
}
try
{
AppLogger.Info("DesktopShell", $"Restoring desktop shell started. Source='{source}'.");
if (_transparentOverlayWindow is not null && _transparentOverlayWindow.IsVisible)
{
_transparentOverlayWindow.Hide();
}
2026-04-15 10:49:04 +08:00
var mainWindow = GetOrCreateMainWindow(desktop, source);
mainWindow.PrepareEnterAnimation();
2026-04-15 10:49:04 +08:00
mainWindow.ShowInTaskbar = true;
2026-03-12 21:01:23 +08:00
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
if (mainWindow.WindowState == WindowState.Minimized)
{
mainWindow.WindowState = WindowState.Normal;
}
if (mainWindow.WindowState != WindowState.FullScreen)
{
mainWindow.WindowState = WindowState.FullScreen;
}
mainWindow.Activate();
mainWindow.Topmost = true;
mainWindow.Topmost = false;
2026-04-15 10:49:04 +08:00
Dispatcher.UIThread.Post(() =>
{
mainWindow.PlayEnterAnimation();
}, DispatcherPriority.Background);
2026-04-15 10:49:04 +08:00
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}");
AppLogger.Info(
"DesktopShell",
$"Desktop restored. Source='{source}'; MainWindowClosed={_mainWindowClosed}; ShowSingleInstanceNotice={showSingleInstanceNotice}; WindowState='{mainWindow.WindowState}'.");
2026-03-12 21:01:23 +08:00
if (showSingleInstanceNotice)
{
mainWindow.ShowSingleInstanceNotice();
}
return true;
}
catch (Exception ex)
{
AppLogger.Warn("DesktopShell", $"Failed to restore desktop shell. Source='{source}'.", ex);
return false;
}
}
private void EnsureTransparentOverlayWindow()
{
if (_transparentOverlayWindow is null)
{
_transparentOverlayWindow = new TransparentOverlayWindow();
_transparentOverlayWindow.RestoreMainWindowRequested += (s, e) =>
{
RestoreOrCreateMainWindow(showSingleInstanceNotice: false, source: "TransparentOverlay");
};
}
}
2026-03-12 21:01:23 +08:00
internal void PrepareForShutdown(bool isRestart, string source)
{
void Mark()
{
_shutdownIntent = isRestart
? ShutdownIntent.RestartRequested
: ShutdownIntent.ExitRequested;
AppLogger.Info(
"DesktopShell",
$"Shutdown intent marked. Intent='{_shutdownIntent}'; Source='{source}'; CurrentShellState='{_desktopShellState}'.");
}
if (Dispatcher.UIThread.CheckAccess())
{
Mark();
return;
}
Dispatcher.UIThread.InvokeAsync(Mark, DispatcherPriority.Send).GetAwaiter().GetResult();
}
internal void ResetShutdownIntent(string source)
{
void Reset()
{
if (_shutdownIntent == ShutdownIntent.None)
{
return;
}
AppLogger.Warn(
"DesktopShell",
$"Shutdown intent cleared without process exit. PreviousIntent='{_shutdownIntent}'; Source='{source}'.");
_shutdownIntent = ShutdownIntent.None;
}
if (Dispatcher.UIThread.CheckAccess())
{
Reset();
return;
}
Dispatcher.UIThread.InvokeAsync(Reset, DispatcherPriority.Send).GetAwaiter().GetResult();
}
2026-03-14 13:36:18 +08:00
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
{
_ = sender;
if (e.Scope != SettingsScope.App)
{
return;
}
Dispatcher.UIThread.Post(() =>
{
var changedKeys = e.ChangedKeys?.ToArray();
var refreshAll = changedKeys is null || changedKeys.Length == 0;
2026-03-15 17:08:07 +08:00
var liveAppearance = _appearanceThemeService.GetCurrent();
2026-03-14 13:36:18 +08:00
var themeChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
2026-03-15 17:08:07 +08:00
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) ||
2026-04-08 00:55:10 +08:00
changedKeys.Contains(nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparer.OrdinalIgnoreCase) ||
2026-03-15 17:08:07 +08:00
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) ||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) &&
(changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase)));
2026-03-14 13:36:18 +08:00
var languageChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
if (themeChanged)
{
ApplyThemeFromSettings();
}
if (languageChanged)
{
2026-03-17 14:57:41 +08:00
// 清除本地化缓存,强制重新加载语言文件
_localizationService.ClearCache();
2026-03-14 13:36:18 +08:00
ApplyCurrentCultureFromSettings();
2026-03-21 16:16:02 +08:00
RefreshTrayIconContent();
2026-03-14 13:36:18 +08:00
}
// 检查融合桌面设置是否变更
var fusedDesktopChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.EnableFusedDesktop), StringComparer.OrdinalIgnoreCase);
if (fusedDesktopChanged)
{
RefreshFusedDesktopMenuItemVisibility();
}
2026-03-14 13:36:18 +08:00
}, DispatcherPriority.Background);
}
2026-03-15 17:08:07 +08:00
private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot e)
2026-03-15 04:35:34 +08:00
{
2026-03-15 17:08:07 +08:00
_ = sender;
_ = e;
2026-03-15 04:35:34 +08:00
2026-03-15 17:08:07 +08:00
Dispatcher.UIThread.Post(ApplyThemeFromSettings, DispatcherPriority.Background);
2026-03-15 04:35:34 +08:00
}
2026-03-15 17:08:07 +08:00
private void ApplyAdaptiveThemeResources()
2026-03-14 13:36:18 +08:00
{
2026-03-15 17:08:07 +08:00
_appearanceThemeService.ApplyThemeResources(Resources);
2026-03-14 13:36:18 +08:00
}
2026-03-13 22:20:12 +08:00
private void RegisterUiUnhandledExceptionGuard()
{
if (_uiUnhandledExceptionHooked)
{
return;
}
Dispatcher.UIThread.UnhandledException += OnUiThreadUnhandledException;
_uiUnhandledExceptionHooked = true;
}
private void OnUiThreadUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e)
{
if (!IsKnownWebViewStartupException(e.Exception))
{
return;
}
e.Handled = true;
AppLogger.Warn(
"WebView2",
"Suppressed a known WebView startup exception from AvaloniaWebView.Navigate to keep the host process alive.",
e.Exception);
}
private static bool IsKnownWebViewStartupException(Exception exception)
{
if (exception is not NullReferenceException)
{
return false;
}
var stackTrace = exception.StackTrace ?? string.Empty;
return stackTrace.Contains("AvaloniaWebView.WebView.Navigate", StringComparison.Ordinal) &&
stackTrace.Contains("AvaloniaWebView.WebView.OnAttachedToVisualTree", StringComparison.Ordinal);
}
private void ReleaseSingleInstanceAfterExit(string source)
{
if (_singleInstanceReleased)
{
return;
}
_singleInstanceReleased = true;
var singleInstance = CurrentSingleInstanceService;
CurrentSingleInstanceService = null;
if (singleInstance is null)
{
AppLogger.Info("SingleInstance", $"No single-instance handle to release. Source='{source}'.");
return;
}
try
{
singleInstance.Dispose();
AppLogger.Info("SingleInstance", $"Released single-instance handle. Source='{source}'.");
}
catch (Exception ex)
{
AppLogger.Warn("SingleInstance", $"Failed to release single-instance handle. Source='{source}'.", ex);
}
}
private void ScheduleForcedProcessTermination(string source)
{
if (Interlocked.Exchange(ref _forcedExitScheduled, 1) != 0)
{
return;
}
_ = Task.Run(async () =>
{
try
{
await Task.Delay(TimeSpan.FromSeconds(8)).ConfigureAwait(false);
AppLogger.Warn(
"DesktopShell",
$"Process did not terminate after desktop exit cleanup. Forcing process exit. Source='{source}'; ShutdownIntent='{_shutdownIntent}'.");
Environment.Exit(0);
}
catch (Exception ex)
{
AppLogger.Warn("DesktopShell", $"Forced process termination scheduler failed. Source='{source}'.", ex);
}
});
}
private void PerformExitCleanup()
{
if (_exitCleanupCompleted)
{
return;
}
_exitCleanupCompleted = true;
2026-03-14 13:36:18 +08:00
_settingsFacade.Settings.Changed -= OnSettingsChanged;
2026-03-15 17:08:07 +08:00
_appearanceThemeService.Changed -= OnAppearanceThemeChanged;
2026-03-16 09:50:48 +08:00
try
{
2026-03-21 16:16:02 +08:00
TelemetryServices.Usage?.Shutdown(
_shutdownIntent == ShutdownIntent.RestartRequested,
"App.PerformExitCleanup");
2026-03-16 09:50:48 +08:00
}
catch (Exception ex)
{
2026-03-21 16:16:02 +08:00
AppLogger.Warn("Analytics", "Failed to shut down usage telemetry during exit cleanup.", ex);
2026-03-16 09:50:48 +08:00
}
2026-03-15 04:35:34 +08:00
try
{
HostUpdateWorkflowServiceProvider.GetOrCreate().TryApplyPendingUpdateOnExit();
}
catch (Exception ex)
{
AppLogger.Warn("UpdateWorkflow", "Failed to apply pending update during exit cleanup.", ex);
}
try
{
2026-03-12 21:01:23 +08:00
_pluginRuntimeService?.Dispose();
}
catch (Exception ex)
{
2026-03-12 21:01:23 +08:00
AppLogger.Warn("PluginRuntime", "Failed to dispose plugin runtime during shutdown.", ex);
}
finally
{
2026-03-12 21:01:23 +08:00
_pluginRuntimeService = null;
}
2026-03-13 22:20:12 +08:00
_settingsWindowService?.Close();
if (_settingsPageRegistry is IDisposable disposableRegistry)
{
disposableRegistry.Dispose();
}
if (_transparentOverlayWindow is not null)
{
try
{
_transparentOverlayWindow.Close();
}
catch (Exception ex)
{
AppLogger.Warn("DesktopShell", "Failed to close transparent overlay during exit cleanup.", ex);
}
finally
{
_transparentOverlayWindow = null;
}
}
2026-03-12 21:01:23 +08:00
AudioRecorderServiceFactory.DisposeSharedServices();
StudyAnalyticsServiceFactory.DisposeSharedService();
DisposeTrayIcon();
2026-03-21 16:16:02 +08:00
try
{
TelemetryServices.Crash?.CaptureShutdown(
_shutdownIntent == ShutdownIntent.RestartRequested,
"App.PerformExitCleanup");
}
catch (Exception ex)
{
AppLogger.Warn("Analytics", "Failed to capture crash shutdown telemetry during exit cleanup.", ex);
}
try
{
TelemetryServices.Crash?.Dispose();
TelemetryServices.Usage?.Dispose();
}
catch (Exception ex)
{
AppLogger.Warn("Analytics", "Failed to dispose telemetry services during exit cleanup.", ex);
}
2026-03-12 21:01:23 +08:00
}
private MainWindow CreateAndAssignMainWindow(
IClassicDesktopStyleApplicationLifetime desktop,
string reason)
{
var mainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
ShowInTaskbar = true
};
AttachMainWindow(mainWindow);
desktop.MainWindow = mainWindow;
AppLogger.Info("App", $"Main window created. Reason='{reason}'. LogFile={AppLogger.LogFilePath}");
LogBrowserStartupDiagnostics();
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"MainWindowCreated:{reason}");
// 延迟报告 Ready 直到窗口实际打开并可见
// 使用 Opened 事件确保所有资源已加载完毕
mainWindow.Opened += OnMainWindowOpened;
2026-04-20 17:42:16 +08:00
// 手动显示窗口,因为在 ShutdownMode.OnExplicitShutdown 模式下框架不会自动调用 Show
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
// 兜底机制:如果 Opened 事件 10 秒内未触发,强制发送 Ready 信号
// 防止因渲染问题导致 Opened 不触发,启动器 Splash 窗口一直显示
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(10));
if (_launcherIpcClient is not null && _launcherIpcClient.IsConnected)
{
try
{
await _launcherIpcClient.ReportProgressAsync(new StartupProgressMessage
{
Stage = StartupStage.Ready,
ProgressPercent = 100,
Message = "就绪"
});
AppLogger.Warn("App", "Ready signal sent via fallback (Opened event did not fire within 10s)");
}
catch { }
}
});
2026-03-12 21:01:23 +08:00
return mainWindow;
}
/// <summary>
/// 主窗口打开完成事件 - 此时所有组件、资源及功能模块均已完全加载
/// </summary>
private void OnMainWindowOpened(object? sender, EventArgs e)
{
if (sender is MainWindow mainWindow)
{
mainWindow.Opened -= OnMainWindowOpened;
AppLogger.Info("App", "Main window opened and ready. Reporting Ready to Launcher...");
// 完成系统初始化加载项
_loadingStateManager?.CompleteItem("system.init", "系统初始化完成");
// 报告 Ready 状态,启动器可以安全关闭 Splash 窗口
ReportStartupProgressSync(StartupStage.Ready, 100, "就绪");
// 停止加载状态上报
_loadingStateReporter?.Stop();
}
}
2026-03-12 21:01:23 +08:00
private MainWindow GetOrCreateMainWindow(
IClassicDesktopStyleApplicationLifetime desktop,
string reason)
{
if (_mainWindow is not null && !_mainWindowClosed)
{
return _mainWindow;
}
if (desktop.MainWindow is MainWindow desktopMainWindow && !_mainWindowClosed)
{
AttachMainWindow(desktopMainWindow);
return desktopMainWindow;
}
return CreateAndAssignMainWindow(desktop, reason);
}
private void AttachMainWindow(MainWindow mainWindow)
{
if (ReferenceEquals(_mainWindow, mainWindow))
{
_mainWindowClosed = false;
return;
}
if (_mainWindow is not null)
{
_mainWindow.Closing -= OnMainWindowClosing;
_mainWindow.Closed -= OnMainWindowClosed;
_mainWindow.PropertyChanged -= OnMainWindowPropertyChanged;
}
_mainWindow = mainWindow;
_mainWindowClosed = false;
mainWindow.Closing += OnMainWindowClosing;
mainWindow.Closed += OnMainWindowClosed;
mainWindow.PropertyChanged += OnMainWindowPropertyChanged;
}
private void OnMainWindowClosing(object? sender, WindowClosingEventArgs e)
{
if (sender is not MainWindow mainWindow)
{
return;
}
AppLogger.Info(
"DesktopShell",
$"Main window closing requested. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'; WindowState='{mainWindow.WindowState}'; IsVisible={mainWindow.IsVisible}.");
if (_shutdownIntent is ShutdownIntent.ExitRequested or ShutdownIntent.RestartRequested)
{
AppLogger.Info(
"DesktopShell",
$"Main window close allowed. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'.");
return;
}
e.Cancel = true;
HideMainWindowToTray(mainWindow, "MainWindowClosing");
}
private void OnMainWindowClosed(object? sender, EventArgs e)
{
if (sender is not MainWindow mainWindow)
{
return;
}
mainWindow.Closing -= OnMainWindowClosing;
mainWindow.Closed -= OnMainWindowClosed;
mainWindow.PropertyChanged -= OnMainWindowPropertyChanged;
if (ReferenceEquals(_mainWindow, mainWindow))
{
_mainWindow = null;
}
_mainWindowClosed = true;
AppLogger.Info(
"DesktopShell",
$"Main window closed. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'.");
if (_shutdownIntent == ShutdownIntent.None)
{
SetDesktopShellState(DesktopShellState.TrayOnly, "MainWindowClosedUnexpected");
}
}
private void OnMainWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is not MainWindow mainWindow)
{
return;
}
if (e.Property != Window.WindowStateProperty)
{
return;
}
if (_shutdownIntent != ShutdownIntent.None || !mainWindow.IsVisible)
{
return;
}
if (mainWindow.WindowState == WindowState.Minimized)
{
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, "MainWindowMinimized");
return;
}
SetDesktopShellState(DesktopShellState.ForegroundDesktop, "MainWindowRestored");
}
internal void HideMainWindowToTray(MainWindow mainWindow, string source)
2026-03-12 21:01:23 +08:00
{
try
{
2026-03-12 21:01:23 +08:00
mainWindow.ShowInTaskbar = false;
mainWindow.Hide();
SetDesktopShellState(DesktopShellState.TrayOnly, source);
AppLogger.Info(
"DesktopShell",
$"Main window hidden to tray. Source='{source}'; WindowState='{mainWindow.WindowState}'.");
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
if (appSnapshot.EnableThreeFingerSwipe && appSnapshot.EnableFusedDesktop)
{
EnsureTransparentOverlayWindow();
_transparentOverlayWindow?.Show();
}
}
catch (Exception ex)
{
2026-03-12 21:01:23 +08:00
AppLogger.Warn("DesktopShell", $"Failed to hide main window to tray. Source='{source}'.", ex);
}
2026-03-12 21:01:23 +08:00
}
private void SetDesktopShellState(DesktopShellState state, string source)
{
if (_desktopShellState == state)
{
2026-03-12 21:01:23 +08:00
return;
}
2026-03-12 21:01:23 +08:00
var previous = _desktopShellState;
_desktopShellState = state;
AppLogger.Info(
"DesktopShell",
$"Shell state changed. Previous='{previous}'; Current='{state}'; Source='{source}'.");
}
private void LogBrowserStartupDiagnostics()
{
try
{
var snapshot = new DesktopLayoutSettingsService().Load();
var browserPlacements = snapshot.DesktopComponentPlacements
.Where(placement => string.Equals(
placement.ComponentId,
BuiltInComponentIds.DesktopBrowser,
StringComparison.OrdinalIgnoreCase))
.ToList();
var runtimeAvailability = WebView2RuntimeProbe.GetAvailability();
AppLogger.Info(
"StartupDiagnostics",
$"Browser component diagnostics. HasBrowserPlacement={browserPlacements.Count > 0}; " +
$"ActivePageHasBrowser={browserPlacements.Any(item => item.PageIndex == snapshot.CurrentDesktopSurfaceIndex)}; " +
$"CurrentDesktopSurfaceIndex={snapshot.CurrentDesktopSurfaceIndex}; " +
$"WebViewRuntimeAvailable={runtimeAvailability.IsAvailable}; " +
$"WebViewRuntimeVersion={runtimeAvailability.Version ?? string.Empty}; " +
$"WebViewRuntimeMessage={runtimeAvailability.Message}");
}
catch (Exception ex)
{
AppLogger.Warn("StartupDiagnostics", "Failed to log browser component diagnostics.", ex);
}
}
2026-03-10 16:35:43 +08:00
private string L(string key, string fallback)
{
2026-03-13 09:10:00 +08:00
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
2026-03-10 16:35:43 +08:00
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
return _localizationService.GetString(languageCode, key, fallback);
}
}