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;
|
2026-03-12 12:25:22 +08:00
|
|
|
|
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;
|
2026-04-18 19:50:33 +08:00
|
|
|
|
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();
|
2026-03-15 21:27:48 +08:00
|
|
|
|
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();
|
2026-03-11 09:40:36 +08:00
|
|
|
|
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;
|
2026-04-02 11:54:58 +08:00
|
|
|
|
private INotificationService? _notificationService;
|
2026-03-11 09:40:36 +08:00
|
|
|
|
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;
|
2026-04-02 21:12:06 +08:00
|
|
|
|
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;
|
2026-04-18 19:50:33 +08:00
|
|
|
|
private LoadingStateManager? _loadingStateManager;
|
|
|
|
|
|
private LoadingStateReporter? _loadingStateReporter;
|
2026-04-19 17:02:53 +08:00
|
|
|
|
private bool _singleInstanceReleased;
|
|
|
|
|
|
private int _forcedExitScheduled;
|
2026-03-09 12:27:33 +08:00
|
|
|
|
|
2026-03-11 09:40:36 +08:00
|
|
|
|
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
|
|
|
|
|
|
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
|
|
|
|
|
|
(Current as App)?._hostApplicationLifecycle;
|
2026-04-02 11:54:58 +08:00
|
|
|
|
internal static INotificationService? CurrentNotificationService =>
|
|
|
|
|
|
(Current as App)?._notificationService;
|
2026-03-11 09:40:36 +08:00
|
|
|
|
|
2026-03-17 01:01:48 +08:00
|
|
|
|
// 隐私政策查看事件
|
|
|
|
|
|
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;
|
2026-03-11 09:40:36 +08:00
|
|
|
|
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
|
2026-03-13 22:20:12 +08:00
|
|
|
|
internal ISettingsWindowService? SettingsWindowService => _settingsWindowService;
|
2026-04-02 11:54:58 +08:00
|
|
|
|
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();
|
2026-03-04 03:41:59 +08:00
|
|
|
|
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();
|
2026-04-02 11:54:58 +08:00
|
|
|
|
EnsureNotificationService();
|
2026-02-26 23:08:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 23:36:31 +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();
|
2026-04-18 23:36:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 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.");
|
2026-04-18 19:50:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化加载状态管理器
|
|
|
|
|
|
_loadingStateManager = new LoadingStateManager();
|
|
|
|
|
|
_loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _launcherIpcClient);
|
|
|
|
|
|
_loadingStateReporter.Start();
|
|
|
|
|
|
|
|
|
|
|
|
// 注册系统初始化加载项
|
|
|
|
|
|
_loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "系统初始化", "初始化系统核心组件");
|
2026-04-18 23:36:31 +08:00
|
|
|
|
_loadingStateManager.StartItem("system.init", "已连接启动器");
|
2026-04-18 19:50:33 +08:00
|
|
|
|
|
2026-04-17 15:16:01 +08:00
|
|
|
|
ReportStartupProgress(StartupStage.Initializing, 10, "正在初始化...");
|
2026-04-18 23:36:31 +08:00
|
|
|
|
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}");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 19:50:33 +08:00
|
|
|
|
/// <summary>
|
2026-04-18 23:36:31 +08:00
|
|
|
|
/// 向 Launcher 报告关键启动进度,使用后台线程避免阻塞 UI
|
2026-04-18 19:50:33 +08:00
|
|
|
|
/// 用于 Ready 等关键状态报告
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void ReportStartupProgressSync(StartupStage stage, int percent, string message)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_launcherIpcClient is null)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-04-18 23:36:31 +08:00
|
|
|
|
_ = Task.Run(async () =>
|
2026-04-18 19:50:33 +08:00
|
|
|
|
{
|
2026-04-18 23:36:31 +08:00
|
|
|
|
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}");
|
|
|
|
|
|
}
|
2026-04-18 19:50:33 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-04-18 23:36:31 +08:00
|
|
|
|
AppLogger.Warn("LauncherIpc", $"Failed to launch progress report task: {ex.Message}");
|
2026-04-18 19:50:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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");
|
|
|
|
|
|
},
|
2026-04-19 17:02:53 +08:00
|
|
|
|
OnDesktopLifetimeExit,
|
2026-03-20 00:41:14 +08:00
|
|
|
|
() => CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow),
|
|
|
|
|
|
StartWeatherLocationRefreshIfNeeded);
|
|
|
|
|
|
_desktopShellHost.Initialize(this);
|
2026-02-26 23:08:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
private void OnDesktopLifetimeExit()
|
|
|
|
|
|
{
|
|
|
|
|
|
AppLogger.Info("App", "Desktop lifetime exit triggered.");
|
|
|
|
|
|
PerformExitCleanup();
|
|
|
|
|
|
ReleaseSingleInstanceAfterExit("DesktopLifetimeExit");
|
|
|
|
|
|
ScheduleForcedProcessTermination("DesktopLifetimeExit");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 10:32:02 +08:00
|
|
|
|
private void OnTrayExitClick(object? sender, EventArgs e)
|
|
|
|
|
|
{
|
2026-03-11 09:40:36 +08:00
|
|
|
|
_ = _hostApplicationLifecycle.TryExit(new HostApplicationLifecycleRequest(
|
|
|
|
|
|
Source: "TrayMenu",
|
|
|
|
|
|
Reason: "User selected Exit App from the tray menu."));
|
2026-03-06 10:32:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
|
2026-03-06 10:32:02 +08:00
|
|
|
|
private void OnTrayRestartClick(object? sender, EventArgs e)
|
|
|
|
|
|
{
|
2026-03-11 09:40:36 +08:00
|
|
|
|
_ = _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;
|
2026-04-02 21:12:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 仅在 Windows 上支持融合桌面功能
|
|
|
|
|
|
if (!OperatingSystem.IsWindows())
|
2026-03-13 22:20:12 +08:00
|
|
|
|
{
|
2026-04-02 21:12:06 +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();
|
2026-04-02 21:12:06 +08:00
|
|
|
|
|
2026-04-03 01:17:47 +08:00
|
|
|
|
// 确保透明覆盖层窗口存在并显示
|
2026-04-02 21:12:06 +08:00
|
|
|
|
EnsureTransparentOverlayWindow();
|
|
|
|
|
|
|
|
|
|
|
|
// 打开融合桌面组件库窗口
|
|
|
|
|
|
Dispatcher.UIThread.Post(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-04-03 01:17:47 +08:00
|
|
|
|
// 确保覆盖层窗口已显示(组件要渲染在上面,必须先 Show)
|
|
|
|
|
|
if (_transparentOverlayWindow is not null && !_transparentOverlayWindow.IsVisible)
|
|
|
|
|
|
{
|
|
|
|
|
|
_transparentOverlayWindow.Show();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 21:12:06 +08:00
|
|
|
|
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();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-02 21:12:06 +08:00
|
|
|
|
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
|
|
|
|
|
2026-04-14 00:22:02 +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
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 00:22:02 +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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 11:54:58 +08:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 09:40:36 +08:00
|
|
|
|
private void ActivateMainWindow()
|
2026-03-12 21:01:23 +08:00
|
|
|
|
{
|
2026-04-19 17:02:53 +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
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 21:12:06 +08:00
|
|
|
|
private void RestoreOrCreateMainWindow(bool showSingleInstanceNotice, string source)
|
2026-03-11 09:40:36 +08:00
|
|
|
|
{
|
2026-04-02 21:12:06 +08:00
|
|
|
|
Dispatcher.UIThread.Post(() =>
|
2026-03-11 09:40:36 +08:00
|
|
|
|
{
|
2026-04-19 17:02:53 +08:00
|
|
|
|
_ = RestoreOrCreateMainWindowCore(showSingleInstanceNotice, source);
|
|
|
|
|
|
}, DispatcherPriority.Send);
|
|
|
|
|
|
}
|
2026-03-11 09:40:36 +08:00
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
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)
|
2026-03-11 09:40:36 +08:00
|
|
|
|
{
|
2026-04-19 17:02:53 +08:00
|
|
|
|
_transparentOverlayWindow.Hide();
|
|
|
|
|
|
}
|
2026-04-15 10:49:04 +08:00
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
var mainWindow = GetOrCreateMainWindow(desktop, source);
|
|
|
|
|
|
mainWindow.PrepareEnterAnimation();
|
2026-04-15 10:49:04 +08:00
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
mainWindow.ShowInTaskbar = true;
|
2026-03-12 21:01:23 +08:00
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
if (!mainWindow.IsVisible)
|
|
|
|
|
|
{
|
|
|
|
|
|
mainWindow.Show();
|
|
|
|
|
|
}
|
2026-03-11 09:40:36 +08:00
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
if (mainWindow.WindowState == WindowState.Minimized)
|
|
|
|
|
|
{
|
|
|
|
|
|
mainWindow.WindowState = WindowState.Normal;
|
|
|
|
|
|
}
|
2026-03-11 09:40:36 +08:00
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
if (mainWindow.WindowState != WindowState.FullScreen)
|
|
|
|
|
|
{
|
|
|
|
|
|
mainWindow.WindowState = WindowState.FullScreen;
|
|
|
|
|
|
}
|
2026-03-12 12:25:22 +08:00
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
mainWindow.Activate();
|
|
|
|
|
|
mainWindow.Topmost = true;
|
|
|
|
|
|
mainWindow.Topmost = false;
|
2026-04-15 10:49:04 +08:00
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
Dispatcher.UIThread.Post(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
mainWindow.PlayEnterAnimation();
|
|
|
|
|
|
}, DispatcherPriority.Background);
|
2026-04-15 10:49:04 +08:00
|
|
|
|
|
2026-04-19 17:02:53 +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
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
if (showSingleInstanceNotice)
|
2026-03-11 09:40:36 +08:00
|
|
|
|
{
|
2026-04-19 17:02:53 +08:00
|
|
|
|
mainWindow.ShowSingleInstanceNotice();
|
2026-03-11 09:40:36 +08:00
|
|
|
|
}
|
2026-04-19 17:02:53 +08:00
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
AppLogger.Warn("DesktopShell", $"Failed to restore desktop shell. Source='{source}'.", ex);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-03-11 09:40:36 +08:00
|
|
|
|
}
|
2026-04-02 21:12:06 +08:00
|
|
|
|
|
|
|
|
|
|
private void EnsureTransparentOverlayWindow()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_transparentOverlayWindow is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_transparentOverlayWindow = new TransparentOverlayWindow();
|
|
|
|
|
|
_transparentOverlayWindow.RestoreMainWindowRequested += (s, e) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
RestoreOrCreateMainWindow(showSingleInstanceNotice: false, source: "TransparentOverlay");
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 09:40:36 +08:00
|
|
|
|
|
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
|
|
|
|
}
|
2026-04-14 00:22:02 +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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 09:40:36 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-03-11 09:40:36 +08:00
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-03-12 21:01:23 +08:00
|
|
|
|
_pluginRuntimeService?.Dispose();
|
2026-03-11 09:40:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-03-12 21:01:23 +08:00
|
|
|
|
AppLogger.Warn("PluginRuntime", "Failed to dispose plugin runtime during shutdown.", ex);
|
2026-03-11 09:40:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
2026-03-12 21:01:23 +08:00
|
|
|
|
_pluginRuntimeService = null;
|
2026-03-11 09:40:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 22:20:12 +08:00
|
|
|
|
_settingsWindowService?.Close();
|
|
|
|
|
|
if (_settingsPageRegistry is IDisposable disposableRegistry)
|
|
|
|
|
|
{
|
|
|
|
|
|
disposableRegistry.Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 17:02:53 +08:00
|
|
|
|
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}");
|
2026-04-18 19:50:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 延迟报告 Ready 直到窗口实际打开并可见
|
|
|
|
|
|
// 使用 Opened 事件确保所有资源已加载完毕
|
|
|
|
|
|
mainWindow.Opened += OnMainWindowOpened;
|
2026-04-20 17:42:16 +08:00
|
|
|
|
|
|
|
|
|
|
// 手动显示窗口,因为在 ShutdownMode.OnExplicitShutdown 模式下框架不会自动调用 Show
|
|
|
|
|
|
if (!mainWindow.IsVisible)
|
|
|
|
|
|
{
|
|
|
|
|
|
mainWindow.Show();
|
|
|
|
|
|
}
|
2026-04-18 19:50:33 +08:00
|
|
|
|
|
2026-04-18 23:36:31 +08:00
|
|
|
|
// 兜底机制:如果 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 19:50:33 +08:00
|
|
|
|
/// <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");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 21:12:06 +08:00
|
|
|
|
internal void HideMainWindowToTray(MainWindow mainWindow, string source)
|
2026-03-12 21:01:23 +08:00
|
|
|
|
{
|
2026-03-11 09:40:36 +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}'.");
|
2026-04-02 21:12:06 +08:00
|
|
|
|
|
|
|
|
|
|
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
2026-04-19 17:02:53 +08:00
|
|
|
|
if (appSnapshot.EnableThreeFingerSwipe && appSnapshot.EnableFusedDesktop)
|
2026-04-02 21:12:06 +08:00
|
|
|
|
{
|
|
|
|
|
|
EnsureTransparentOverlayWindow();
|
|
|
|
|
|
_transparentOverlayWindow?.Show();
|
|
|
|
|
|
}
|
2026-03-11 09:40:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
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-11 09:40:36 +08:00
|
|
|
|
}
|
2026-03-12 21:01:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SetDesktopShellState(DesktopShellState state, string source)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_desktopShellState == state)
|
2026-03-11 09:40:36 +08:00
|
|
|
|
{
|
2026-03-12 21:01:23 +08:00
|
|
|
|
return;
|
2026-03-11 09:40:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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}'.");
|
2026-03-11 09:40:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 12:25:22 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-03-04 03:41:59 +08:00
|
|
|
|
}
|