using System; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core; using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Threading; using AvaloniaWebView; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Theme; using LanMountainDesktop.ViewModels; using LanMountainDesktop.Views; namespace LanMountainDesktop; public partial class App : Application { private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6"); private enum DesktopShellState { ForegroundDesktop = 0, MinimizedToTaskbar = 1, TrayOnly = 2 } private enum ShutdownIntent { None = 0, ExitRequested = 1, RestartRequested = 2 } private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); private readonly IAppearanceThemeService _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate(); private readonly IAppLogoService _appLogoService = HostAppLogoProvider.GetOrCreate(); private readonly LocalizationService _localizationService = new(); private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService(); private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService(); private readonly ILocationService _locationService = HostLocationServiceProvider.GetOrCreate(); private ISettingsPageRegistry? _settingsPageRegistry; private ISettingsWindowService? _settingsWindowService; private WeatherLocationRefreshService? _weatherLocationRefreshService; private bool _exitCleanupCompleted; private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop; private ShutdownIntent _shutdownIntent; private TrayIcons? _trayIcons; private PluginRuntimeService? _pluginRuntimeService; private MainWindow? _mainWindow; private bool _mainWindowClosed; private bool _uiUnhandledExceptionHooked; internal static SingleInstanceService? CurrentSingleInstanceService { get; set; } internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle => (Current as App)?._hostApplicationLifecycle; public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService; public ISettingsFacadeService SettingsFacade => _settingsFacade; public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle; internal ISettingsWindowService? SettingsWindowService => _settingsWindowService; internal void OpenIndependentSettingsModule(string source, string? pageTag = null) { EnsureSettingsWindowService(); AppLogger.Info( "SettingsFacade", $"Opening settings window. Source='{source}'; PageTag='{pageTag ?? ""}'."); _settingsWindowService?.Open(new SettingsWindowOpenRequest( Source: source, Owner: _mainWindow is { IsVisible: true } ? _mainWindow : null, PageId: pageTag)); } public App() { _settingsFacade.Settings.Changed += OnSettingsChanged; _appearanceThemeService.Changed += OnAppearanceThemeChanged; } public override void Initialize() { AppLogger.Info("App", "Initializing application resources."); ConfigureWebViewUserDataFolder(); AvaloniaWebViewBuilder.Initialize(default); AvaloniaXamlLoader.Load(this); ApplyThemeFromSettings(); ApplyCurrentCultureFromSettings(); EnsureSettingsWindowService(); EnsureWeatherLocationRefreshService(); } public override void OnFrameworkInitializationCompleted() { AppLogger.Info("App", "Framework initialization completed."); RegisterUiUnhandledExceptionGuard(); LinuxDesktopEntryInstaller.EnsureInstalled(); InitializePluginRuntime(); InitializeTrayIcon(); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime 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; desktop.Exit += (_, _) => { AppLogger.Info("App", "Desktop lifetime exit triggered."); PerformExitCleanup(); }; CreateAndAssignMainWindow(desktop, "FrameworkInitialization"); CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow); } StartWeatherLocationRefreshIfNeeded(); base.OnFrameworkInitializationCompleted(); } private void OnTrayExitClick(object? sender, EventArgs e) { _ = _hostApplicationLifecycle.TryExit(new HostApplicationLifecycleRequest( Source: "TrayMenu", Reason: "User selected Exit App from the tray menu.")); } private void OnTrayShowDesktopClick(object? sender, EventArgs e) { RestoreOrCreateMainWindow(showSingleInstanceNotice: false, source: "TrayMenu"); } private void OnTrayRestartClick(object? sender, EventArgs e) { _ = _hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest( Source: "TrayMenu", Reason: "User selected Restart App from the tray menu.")); } private void OnTraySettingsClick(object? sender, EventArgs e) { _ = sender; _ = e; OpenIndependentSettingsModule("TrayMenu"); } private void OnTrayComponentLibraryClick(object? sender, EventArgs e) { _ = sender; _ = e; if (_mainWindow is null) { return; } _detachedComponentLibraryWindowService.Open(_mainWindow); } private void DisableAvaloniaDataAnnotationValidation() { // Get an array of plugins to remove var dataValidationPluginsToRemove = BindingPlugins.DataValidators.OfType().ToArray(); // remove each entry found foreach (var plugin in dataValidationPluginsToRemove) { BindingPlugins.DataValidators.Remove(plugin); } } 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); } catch (Exception ex) { // Keep startup resilient if user profile folders are unavailable. AppLogger.Warn("WebView2", "Failed to configure WebView2 user data folder.", ex); } } private void InitializePluginRuntime() { try { _pluginRuntimeService?.Dispose(); _pluginRuntimeService = new PluginRuntimeService(_settingsFacade); HostSettingsFacadeProvider.BindPluginRuntime(_pluginRuntimeService); _pluginRuntimeService.LoadInstalledPlugins(); } catch (Exception ex) { AppLogger.Warn("PluginRuntime", "Failed to initialize plugin runtime.", ex); } } private void InitializeTrayIcon() { try { DisposeTrayIcon(); var trayIcon = new TrayIcon { Icon = _appLogoService.CreateTrayIcon(), ToolTipText = L("tray.tooltip", "LanMountainDesktop"), Menu = BuildTrayMenu(), IsVisible = true }; _trayIcons = [trayIcon]; TrayIcon.SetIcons(this, _trayIcons); } catch (Exception ex) { AppLogger.Warn("TrayIcon", "Failed to initialize tray icon.", ex); } } private NativeMenu BuildTrayMenu() { var menu = new NativeMenu(); var showDesktopItem = new NativeMenuItem(L("tray.menu.show_desktop", "Open Desktop")); showDesktopItem.Click += OnTrayShowDesktopClick; menu.Items.Add(showDesktopItem); var settingsItem = new NativeMenuItem(L("tray.menu.settings", "Settings")); settingsItem.Click += OnTraySettingsClick; menu.Items.Add(settingsItem); var componentLibraryItem = new NativeMenuItem(L("tray.menu.component_library", "Component Library")); componentLibraryItem.Click += OnTrayComponentLibraryClick; menu.Items.Add(componentLibraryItem); menu.Items.Add(new NativeMenuItemSeparator()); var restartItem = new NativeMenuItem(L("tray.menu.restart", "Restart App")); restartItem.Click += OnTrayRestartClick; menu.Items.Add(restartItem); menu.Items.Add(new NativeMenuItemSeparator()); var exitItem = new NativeMenuItem(L("tray.menu.exit", "Exit App")); exitItem.Click += OnTrayExitClick; menu.Items.Add(exitItem); return menu; } private void DisposeTrayIcon() { if (_trayIcons is null) { return; } TrayIcon.SetIcons(this, null); foreach (var trayIcon in _trayIcons) { trayIcon.Dispose(); } _trayIcons = null; } private void EnsureSettingsWindowService() { _settingsPageRegistry ??= new SettingsPageRegistry( _settingsFacade, _hostApplicationLifecycle, _localizationService, () => _pluginRuntimeService); _settingsWindowService ??= new SettingsWindowService( _settingsPageRegistry, _hostApplicationLifecycle, _settingsFacade); } private void EnsureWeatherLocationRefreshService() { _weatherLocationRefreshService ??= new WeatherLocationRefreshService( _settingsFacade, _locationService, _localizationService); } 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); } }); } private void ApplyThemeFromSettings() { var snapshot = _appearanceThemeService.GetCurrent(); RequestedThemeVariant = snapshot.IsNightMode ? ThemeVariant.Dark : ThemeVariant.Light; ApplyAdaptiveThemeResources(); } private void ApplyCurrentCultureFromSettings() { var snapshot = _settingsFacade.Settings.LoadSnapshot(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; } private void ActivateMainWindow() { RestoreOrCreateMainWindow(showSingleInstanceNotice: true, source: "SingleInstance"); } private void RestoreOrCreateMainWindow(bool showSingleInstanceNotice, string source) { Dispatcher.UIThread.Post(() => { if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop) { return; } try { var mainWindow = GetOrCreateMainWindow(desktop, source); mainWindow.ShowInTaskbar = true; 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; SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}"); AppLogger.Info( "DesktopShell", $"Desktop restored. Source='{source}'; MainWindowClosed={_mainWindowClosed}; ShowSingleInstanceNotice={showSingleInstanceNotice}; WindowState='{mainWindow.WindowState}'."); if (showSingleInstanceNotice) { mainWindow.ShowSingleInstanceNotice(); } } catch (Exception ex) { AppLogger.Warn("DesktopShell", $"Failed to restore desktop shell. Source='{source}'.", ex); } }, DispatcherPriority.Send); } 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(); } 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; var liveAppearance = _appearanceThemeService.GetCurrent(); var themeChanged = refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) || changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) || (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))); var languageChanged = refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase); if (themeChanged) { ApplyThemeFromSettings(); } if (languageChanged) { ApplyCurrentCultureFromSettings(); if (_trayIcons is not null) { InitializeTrayIcon(); } } }, DispatcherPriority.Background); } private void OnAppearanceThemeChanged(object? sender, AppearanceThemeSnapshot e) { _ = sender; _ = e; Dispatcher.UIThread.Post(ApplyThemeFromSettings, DispatcherPriority.Background); } private void ApplyAdaptiveThemeResources() { _appearanceThemeService.ApplyThemeResources(Resources); } 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 PerformExitCleanup() { if (_exitCleanupCompleted) { return; } _exitCleanupCompleted = true; _settingsFacade.Settings.Changed -= OnSettingsChanged; _appearanceThemeService.Changed -= OnAppearanceThemeChanged; try { HostUpdateWorkflowServiceProvider.GetOrCreate().TryApplyPendingUpdateOnExit(); } catch (Exception ex) { AppLogger.Warn("UpdateWorkflow", "Failed to apply pending update during exit cleanup.", ex); } try { _pluginRuntimeService?.Dispose(); } catch (Exception ex) { AppLogger.Warn("PluginRuntime", "Failed to dispose plugin runtime during shutdown.", ex); } finally { _pluginRuntimeService = null; } _settingsWindowService?.Close(); if (_settingsPageRegistry is IDisposable disposableRegistry) { disposableRegistry.Dispose(); } AudioRecorderServiceFactory.DisposeSharedServices(); StudyAnalyticsServiceFactory.DisposeSharedService(); DisposeTrayIcon(); } 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}"); return mainWindow; } 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"); } private void HideMainWindowToTray(MainWindow mainWindow, string source) { try { mainWindow.ShowInTaskbar = false; mainWindow.Hide(); SetDesktopShellState(DesktopShellState.TrayOnly, source); AppLogger.Info( "DesktopShell", $"Main window hidden to tray. Source='{source}'; WindowState='{mainWindow.WindowState}'."); } catch (Exception ex) { AppLogger.Warn("DesktopShell", $"Failed to hide main window to tray. Source='{source}'.", ex); } } private void SetDesktopShellState(DesktopShellState state, string source) { if (_desktopShellState == state) { return; } 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); } } private string L(string key, string fallback) { var snapshot = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App); var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode); return _localizationService.GetString(languageCode, key, fallback); } }