mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
* 激进的更新 * 试试 * fix.可爱的我一直在修CI( * fix.启动器一定要能够启动 * feat.尝试弄了AOT的启动器。 * fix.修CI,好像是因为Linux那边有个问题,反正修就对了。 * fix.ci难修,为什么liunx跑不起来呢? * Update build.yml * Update LanMountainDesktop.csproj * changed.调整了启动逻辑,优化了更新页面。 * changed.优化了更新体验 * feat.依旧试增量更新这一块,看看velopack * fix.我们试验性地修复了启动器无法正常启动的问题,原因可能是这个画面没有启动,就GUI没显示。然后还把编译问题修了一下。 * fix.继续修ci,ci怎么天天炸 * changed.velopack,试试rust * fix.修ci,修融合桌面,修启动器 * fix.GitHub Action工作流怎么天天出问题 * feat.引入velopack,不好,是rust(至少内存很安全了。 * chore: migrate release pipeline to signed filemap and wire rainyun s3 * fix: make optional s3 upload step workflow-parse safe * fix: make delta pack generation robust for empty diffs and linux paths * chore: rotate launcher update public key for pdc signing * fix: restore stable launcher update public key * fix: sync launcher public key with update signing secret * fix: normalize PEM line endings in signing key validation * fix: rotate launcher public key to match ci signing secret * fix: compare signing keys by SPKI instead of PEM text * refactor update backend to host-managed PDC pipeline * fix release workflow env key collisions * relax publish-pdc precheck to require S3 only * set GH_TOKEN for PDCC installer step * ci: add local pdc mock fallback for release publish * ci: fix pdc mock process log redirection * ci: fallback pdcc signing key to update private key * ci: ensure pdcc signing passphrase env is always set * ci: create pdcc publish root before invoking client * ci: set pdcc version variable from release version * ci: decouple pdcc installer version from publish config version * ci: package pdcc subchannels with generated filemap and changelog * ci: make local pdc mock diff return empty for fast fallback * ci: fix pdcc variable mapping and pdc signing prechecks * Update App.axaml.cs * ci: wire aws cli credentials for rainyun s3 * ci: pin pdcc client version separately from app version * ci: harden local pdc mock transport handling * ci: publish pdcc subchannels in one pass * ci: add pdcc publish heartbeat and timeout * ci: fix pdcc publish workdir bootstrap * feat.Penguin Logistics Online Network Distribution System * ci: fix plonds s3 probe and signing fallback * ci: validate signing key and quiet missing baselines * ci: relax aws checksum mode for rainyun s3 * ci: avoid multipart uploads to rainyun s3 * ci: handle empty plonds baselines safely * ci.plonds * Rebuild release pipeline around PLONDS and DDSS * Fix Windows installer script path in release workflow
1330 lines
45 KiB
C#
1330 lines
45 KiB
C#
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.DesktopHost;
|
||
using LanMountainDesktop.Models;
|
||
using LanMountainDesktop.PluginSdk;
|
||
using LanMountainDesktop.Services;
|
||
using LanMountainDesktop.Services.Launcher;
|
||
using LanMountainDesktop.Services.Loading;
|
||
using LanMountainDesktop.Services.Settings;
|
||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||
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 FontFamilyService _fontFamilyService = 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 INotificationService? _notificationService;
|
||
private bool _exitCleanupCompleted;
|
||
private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop;
|
||
private ShutdownIntent _shutdownIntent;
|
||
|
||
private TrayIcon? _trayIcon;
|
||
private NativeMenuItem? _trayShowDesktopMenuItem;
|
||
private NativeMenuItem? _traySettingsMenuItem;
|
||
private NativeMenuItem? _trayComponentLibraryMenuItem;
|
||
private NativeMenuItem? _trayRestartMenuItem;
|
||
private NativeMenuItem? _trayExitMenuItem;
|
||
private PluginRuntimeService? _pluginRuntimeService;
|
||
private MainWindow? _mainWindow;
|
||
private TransparentOverlayWindow? _transparentOverlayWindow;
|
||
private bool _mainWindowClosed;
|
||
private bool _uiUnhandledExceptionHooked;
|
||
private DesktopShellHost? _desktopShellHost;
|
||
private LauncherIpcClient? _launcherIpcClient;
|
||
private LoadingStateManager? _loadingStateManager;
|
||
private LoadingStateReporter? _loadingStateReporter;
|
||
private bool _singleInstanceReleased;
|
||
private int _forcedExitScheduled;
|
||
|
||
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();
|
||
}
|
||
|
||
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
|
||
public ISettingsFacadeService SettingsFacade => _settingsFacade;
|
||
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
|
||
internal ISettingsWindowService? SettingsWindowService => _settingsWindowService;
|
||
internal INotificationService? NotificationService => _notificationService;
|
||
|
||
internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
|
||
{
|
||
EnsureSettingsWindowService();
|
||
AppLogger.Info(
|
||
"SettingsFacade",
|
||
$"Opening settings window. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
|
||
_settingsWindowService?.Open(new SettingsWindowOpenRequest(
|
||
Source: source,
|
||
Owner: _mainWindow is { IsVisible: true } ? _mainWindow : null,
|
||
PageId: pageTag));
|
||
}
|
||
|
||
public App()
|
||
{
|
||
if (Design.IsDesignMode)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_settingsFacade.Settings.Changed += OnSettingsChanged;
|
||
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
|
||
}
|
||
|
||
public override void Initialize()
|
||
{
|
||
AppLogger.Info("App", "Initializing application resources.");
|
||
AvaloniaXamlLoader.Load(this);
|
||
|
||
if (Design.IsDesignMode)
|
||
{
|
||
ApplyDesignTimeTheme();
|
||
return;
|
||
}
|
||
|
||
ConfigureWebViewUserDataFolder();
|
||
AvaloniaWebViewBuilder.Initialize(default);
|
||
ApplyThemeFromSettings();
|
||
ApplyCurrentCultureFromSettings();
|
||
EnsureSettingsWindowService();
|
||
EnsureWeatherLocationRefreshService();
|
||
EnsureNotificationService();
|
||
}
|
||
|
||
public override void OnFrameworkInitializationCompleted()
|
||
{
|
||
if (Design.IsDesignMode)
|
||
{
|
||
base.OnFrameworkInitializationCompleted();
|
||
return;
|
||
}
|
||
|
||
AppLogger.Info("App", "Framework initialization completed.");
|
||
|
||
RegisterUiUnhandledExceptionGuard();
|
||
LinuxDesktopEntryInstaller.EnsureInstalled();
|
||
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
|
||
|
||
if (!Design.IsDesignMode && OperatingSystem.IsWindows())
|
||
{
|
||
FusedDesktopManagerServiceFactory.GetOrCreate().Initialize();
|
||
}
|
||
|
||
base.OnFrameworkInitializationCompleted();
|
||
|
||
// IPC 初始化移到窗口创建之后,避免 async void 中的 await 导致窗口创建延迟
|
||
// 使用 fire-and-forget 模式,不阻塞主流程
|
||
_ = InitializeLauncherIpcAsync();
|
||
}
|
||
|
||
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", "已连接启动器");
|
||
|
||
ReportStartupProgress(StartupStage.Initializing, 10, "正在初始化...");
|
||
ReportStartupProgress(StartupStage.LoadingSettings, 20, "正在加载设置...");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppLogger.Warn("LauncherIpc", $"Failed to initialize Launcher IPC: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <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}");
|
||
}
|
||
}
|
||
|
||
private void ApplyDesignTimeTheme()
|
||
{
|
||
RequestedThemeVariant = ThemeVariant.Light;
|
||
|
||
try
|
||
{
|
||
ApplyAdaptiveThemeResources();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppLogger.Warn("Previewer", "Failed to apply adaptive theme resources in design mode.", ex);
|
||
}
|
||
}
|
||
|
||
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;
|
||
ReportStartupProgress(StartupStage.InitializingUI, 60, "正在初始化界面...");
|
||
CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
|
||
},
|
||
OnDesktopLifetimeExit,
|
||
() => CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow),
|
||
StartWeatherLocationRefreshIfNeeded);
|
||
_desktopShellHost.Initialize(this);
|
||
}
|
||
|
||
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."));
|
||
}
|
||
|
||
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;
|
||
|
||
// 仅在 Windows 上支持融合桌面功能
|
||
if (!OperatingSystem.IsWindows())
|
||
{
|
||
AppLogger.Warn("FusedDesktop", "Fused desktop is only supported on Windows.");
|
||
return;
|
||
}
|
||
|
||
// 切换进入编辑模式,隐藏常态零散的小部件
|
||
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);
|
||
}
|
||
|
||
// 当组件库关闭时,退出编辑态
|
||
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);
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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()
|
||
{
|
||
ReportStartupProgress(StartupStage.LoadingPlugins, 30, "正在加载插件...");
|
||
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
|
||
{
|
||
if (_trayIcon is null)
|
||
{
|
||
_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);
|
||
|
||
_trayIcon = new TrayIcon
|
||
{
|
||
Icon = _appLogoService.CreateTrayIcon(),
|
||
Menu = trayMenu,
|
||
IsVisible = true
|
||
};
|
||
|
||
TrayIcon.SetIcons(this, [_trayIcon]);
|
||
}
|
||
|
||
RefreshTrayIconContent();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppLogger.Warn("TrayIcon", "Failed to initialize tray icon.", ex);
|
||
}
|
||
}
|
||
|
||
private void RefreshTrayIconContent()
|
||
{
|
||
if (_trayIcon is not null)
|
||
{
|
||
_trayIcon.IsVisible = true;
|
||
if (!OperatingSystem.IsLinux())
|
||
{
|
||
_trayIcon.ToolTipText = L("tray.tooltip", "LanMountainDesktop");
|
||
}
|
||
}
|
||
|
||
if (_trayShowDesktopMenuItem is not null)
|
||
{
|
||
_trayShowDesktopMenuItem.Header = L("tray.menu.show_desktop", "Open Desktop");
|
||
}
|
||
|
||
if (_traySettingsMenuItem is not null)
|
||
{
|
||
_traySettingsMenuItem.Header = L("tray.menu.settings", "Settings");
|
||
}
|
||
|
||
RefreshFusedDesktopMenuItemVisibility();
|
||
|
||
if (_trayRestartMenuItem is not null)
|
||
{
|
||
_trayRestartMenuItem.Header = L("tray.menu.restart", "Restart App");
|
||
}
|
||
|
||
if (_trayExitMenuItem is not null)
|
||
{
|
||
_trayExitMenuItem.Header = L("tray.menu.exit", "Exit App");
|
||
}
|
||
}
|
||
|
||
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");
|
||
}
|
||
}
|
||
|
||
private void DisposeTrayIcon()
|
||
{
|
||
if (_trayIcon is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
_trayIcon.IsVisible = false;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppLogger.Warn("TrayIcon", "Failed to hide tray icon during cleanup.", ex);
|
||
}
|
||
}
|
||
|
||
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 EnsureNotificationService()
|
||
{
|
||
_notificationService ??= new NotificationService(_appearanceThemeService);
|
||
}
|
||
|
||
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<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;
|
||
|
||
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;
|
||
}
|
||
|
||
private void ActivateMainWindow()
|
||
{
|
||
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;
|
||
}
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
var mainWindow = GetOrCreateMainWindow(desktop, source);
|
||
mainWindow.PrepareEnterAnimation();
|
||
|
||
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;
|
||
|
||
Dispatcher.UIThread.Post(() =>
|
||
{
|
||
mainWindow.PlayEnterAnimation();
|
||
}, DispatcherPriority.Background);
|
||
|
||
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}");
|
||
AppLogger.Info(
|
||
"DesktopShell",
|
||
$"Desktop restored. Source='{source}'; MainWindowClosed={_mainWindowClosed}; ShowSingleInstanceNotice={showSingleInstanceNotice}; WindowState='{mainWindow.WindowState}'.");
|
||
|
||
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");
|
||
};
|
||
}
|
||
}
|
||
|
||
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) ||
|
||
changedKeys.Contains(nameof(AppSettingsSnapshot.CornerRadiusStyle), 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)
|
||
{
|
||
// 清除本地化缓存,强制重新加载语言文件
|
||
_localizationService.ClearCache();
|
||
ApplyCurrentCultureFromSettings();
|
||
RefreshTrayIconContent();
|
||
}
|
||
|
||
// 检查融合桌面设置是否变更
|
||
var fusedDesktopChanged =
|
||
refreshAll ||
|
||
changedKeys.Contains(nameof(AppSettingsSnapshot.EnableFusedDesktop), StringComparer.OrdinalIgnoreCase);
|
||
|
||
if (fusedDesktopChanged)
|
||
{
|
||
RefreshFusedDesktopMenuItemVisibility();
|
||
}
|
||
}, 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 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;
|
||
_settingsFacade.Settings.Changed -= OnSettingsChanged;
|
||
_appearanceThemeService.Changed -= OnAppearanceThemeChanged;
|
||
|
||
try
|
||
{
|
||
TelemetryServices.Usage?.Shutdown(
|
||
_shutdownIntent == ShutdownIntent.RestartRequested,
|
||
"App.PerformExitCleanup");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppLogger.Warn("Analytics", "Failed to shut down usage telemetry during exit cleanup.", ex);
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
AudioRecorderServiceFactory.DisposeSharedServices();
|
||
StudyAnalyticsServiceFactory.DisposeSharedService();
|
||
DisposeTrayIcon();
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
// 手动显示窗口,因为在 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 { }
|
||
}
|
||
});
|
||
|
||
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();
|
||
}
|
||
}
|
||
|
||
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)
|
||
{
|
||
try
|
||
{
|
||
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)
|
||
{
|
||
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<AppSettingsSnapshot>(SettingsScope.App);
|
||
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||
return _localizationService.GetString(languageCode, key, fallback);
|
||
}
|
||
}
|