From 60e7f31ba785c1cc0b3e53de35e7a0e6f9368691 Mon Sep 17 00:00:00 2001 From: lincube Date: Mon, 4 May 2026 11:22:21 +0800 Subject: [PATCH] Add OOBE startup presentation and settings merge Introduce a new OOBE step for "Startup & Presentation" that exposes startup and UI preferences in OobeWindow (toggles for taskbar, slide/fade transitions, fused popup, and autostart). Add HostAppSettingsOobeMerger to read/write Host settings.json (PascalCase fields) and MergeStartupPresentation behavior, plus LauncherWindowsStartupService to sync the current Launcher into the Windows Run key on Windows. Wire UI handlers, persist choices on Next, and load defaults when entering the step. Include unit tests for the merger, adjust SettingsWindow navigation pane/toggle handling, and update docs/LAUNCHER.md to describe the new OOBE step and implementation files. --- .../checklist.md | 1 + .../Services/HostAppSettingsOobeMerger.cs | 134 +++++++++++ .../Services/LauncherWindowsStartupService.cs | 82 +++++++ .../Views/OobeWindow.axaml | 139 ++++++++++- .../Views/OobeWindow.axaml.cs | 225 +++++++++++++++++- .../HostAppSettingsOobeMergerTests.cs | 91 +++++++ LanMountainDesktop/Views/SettingsWindow.axaml | 18 +- .../Views/SettingsWindow.axaml.cs | 73 ++++-- docs/LAUNCHER.md | 2 + 9 files changed, 717 insertions(+), 48 deletions(-) create mode 100644 LanMountainDesktop.Launcher/Services/HostAppSettingsOobeMerger.cs create mode 100644 LanMountainDesktop.Launcher/Services/LauncherWindowsStartupService.cs create mode 100644 LanMountainDesktop.Tests/HostAppSettingsOobeMergerTests.cs diff --git a/.trae/specs/launcher-oobe-elevation-hardening/checklist.md b/.trae/specs/launcher-oobe-elevation-hardening/checklist.md index 1afaff9..b9003ac 100644 --- a/.trae/specs/launcher-oobe-elevation-hardening/checklist.md +++ b/.trae/specs/launcher-oobe-elevation-hardening/checklist.md @@ -6,3 +6,4 @@ - [ ] `apply-update` and `plugin-install` do not auto-enter OOBE. - [ ] Default plugin install does not request UAC. - [ ] Logs include OOBE status, suppression reason, and launch source. +- [ ] Startup presentation step inside `OobeWindow` (after data location) writes host `settings.json` and syncs Windows Run when autostart is chosen (Launcher executable). diff --git a/LanMountainDesktop.Launcher/Services/HostAppSettingsOobeMerger.cs b/LanMountainDesktop.Launcher/Services/HostAppSettingsOobeMerger.cs new file mode 100644 index 0000000..04a4b9b --- /dev/null +++ b/LanMountainDesktop.Launcher/Services/HostAppSettingsOobeMerger.cs @@ -0,0 +1,134 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using LanMountainDesktop.Shared.Contracts.Launcher; + +namespace LanMountainDesktop.Launcher.Services; + +/// +/// 在 OOBE 中向 Host 的 settings.json 写入启动与展示相关字段,属性名与 Host +/// AppSettingsSnapshot 的 JSON 序列化一致(PascalCase)。 +/// +public static class HostAppSettingsOobeMerger +{ + public const string ShowInTaskbarKey = "ShowInTaskbar"; + public const string EnableFadeTransitionKey = "EnableFadeTransition"; + public const string EnableSlideTransitionKey = "EnableSlideTransition"; + public const string EnableFusedDesktopKey = "EnableFusedDesktop"; + public const string EnableThreeFingerSwipeKey = "EnableThreeFingerSwipe"; + public const string AutoStartWithWindowsKey = "AutoStartWithWindows"; + + public static string GetSettingsFilePath(string dataRoot) => + Path.Combine(Path.GetFullPath(dataRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)), "settings.json"); + + public static HostAppSettingsStartupDefaults LoadStartupDefaults(string settingsPath) + { + if (!File.Exists(settingsPath)) + { + return HostAppSettingsStartupDefaults.Fallback; + } + + try + { + var root = JsonNode.Parse(File.ReadAllText(settingsPath))?.AsObject(); + if (root is null) + { + return HostAppSettingsStartupDefaults.Fallback; + } + + var fade = ReadBool(root, EnableFadeTransitionKey, defaultValue: true); + var slide = ReadBool(root, EnableSlideTransitionKey, defaultValue: false); + var normalized = StartupVisualPreferencesResolver.FromFlags(fade, slide); + + return new HostAppSettingsStartupDefaults( + ShowInTaskbar: ReadBool(root, ShowInTaskbarKey, defaultValue: false), + EnableFadeTransition: normalized.EnableFadeTransition, + EnableSlideTransition: normalized.EnableSlideTransition, + FusedPopupExperience: ReadBool(root, EnableFusedDesktopKey, defaultValue: false) && + ReadBool(root, EnableThreeFingerSwipeKey, defaultValue: false), + AutoStartWithWindows: ReadBool(root, AutoStartWithWindowsKey, defaultValue: false)); + } + catch (Exception ex) + { + Logger.Warn($"HostAppSettingsOobeMerger: failed to read '{settingsPath}'. {ex.Message}"); + return HostAppSettingsStartupDefaults.Fallback; + } + } + + public static void MergeStartupPresentation(string settingsPath, HostAppSettingsStartupChoices choices) + { + var directory = Path.GetDirectoryName(settingsPath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + JsonObject root; + if (File.Exists(settingsPath)) + { + try + { + root = JsonNode.Parse(File.ReadAllText(settingsPath))?.AsObject() ?? new JsonObject(); + } + catch (Exception ex) + { + Logger.Warn($"HostAppSettingsOobeMerger: replacing invalid JSON at '{settingsPath}'. {ex.Message}"); + root = new JsonObject(); + } + } + else + { + root = new JsonObject(); + } + + var normalized = StartupVisualPreferencesResolver.FromFlags( + choices.EnableFadeTransition, + choices.EnableSlideTransition); + + root[ShowInTaskbarKey] = choices.ShowInTaskbar; + root[EnableFadeTransitionKey] = normalized.EnableFadeTransition; + root[EnableSlideTransitionKey] = normalized.EnableSlideTransition; + root[EnableFusedDesktopKey] = choices.FusedPopupExperience; + root[EnableThreeFingerSwipeKey] = choices.FusedPopupExperience; + root[AutoStartWithWindowsKey] = choices.AutoStartWithWindows; + + var options = new JsonSerializerOptions { WriteIndented = true }; + File.WriteAllText(settingsPath, root.ToJsonString(options)); + } + + private static bool ReadBool(JsonObject root, string key, bool defaultValue) + { + if (!root.TryGetPropertyValue(key, out var node) || node is null) + { + return defaultValue; + } + + return node switch + { + JsonValue v when v.TryGetValue(out var b) => b, + JsonValue v when v.TryGetValue(out var s) => bool.TryParse(s, out var p) && p, + _ => defaultValue + }; + } +} + +public readonly record struct HostAppSettingsStartupDefaults( + bool ShowInTaskbar, + bool EnableFadeTransition, + bool EnableSlideTransition, + bool FusedPopupExperience, + bool AutoStartWithWindows) +{ + public static HostAppSettingsStartupDefaults Fallback { get; } = new( + ShowInTaskbar: false, + EnableFadeTransition: true, + EnableSlideTransition: false, + FusedPopupExperience: false, + AutoStartWithWindows: false); +} + +public readonly record struct HostAppSettingsStartupChoices( + bool ShowInTaskbar, + bool EnableFadeTransition, + bool EnableSlideTransition, + bool FusedPopupExperience, + bool AutoStartWithWindows); diff --git a/LanMountainDesktop.Launcher/Services/LauncherWindowsStartupService.cs b/LanMountainDesktop.Launcher/Services/LauncherWindowsStartupService.cs new file mode 100644 index 0000000..d94a912 --- /dev/null +++ b/LanMountainDesktop.Launcher/Services/LauncherWindowsStartupService.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.Win32; + +namespace LanMountainDesktop.Launcher.Services; + +/// +/// 将当前 Windows 用户登录时自启动项指向本 Launcher 进程(与正式入口一致)。 +/// Host 内 WindowsStartupService 使用 Host 进程路径; +/// OOBE 在 Launcher 内执行时应使用本类型,以便开机后仍走更新/版本协调流程。 +/// +public sealed class LauncherWindowsStartupService +{ + private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; + private const string ValueName = "LanMountainDesktop"; + private readonly string _startupCommand; + + public LauncherWindowsStartupService() + { + var processPath = Environment.ProcessPath; + _startupCommand = string.IsNullOrWhiteSpace(processPath) + ? string.Empty + : $"\"{processPath}\""; + } + + public bool IsEnabled() + { + if (!OperatingSystem.IsWindows()) + { + return false; + } + + try + { + using var runKey = Registry.CurrentUser.OpenSubKey(RunKeyPath, writable: false); + return runKey?.GetValue(ValueName) is string value && + !string.IsNullOrWhiteSpace(value); + } + catch (Exception ex) + { + Logger.Warn($"LauncherWindowsStartup: failed to read Run key. {ex.Message}"); + return false; + } + } + + public bool SetEnabled(bool enabled) + { + if (!OperatingSystem.IsWindows()) + { + return false; + } + + if (enabled && string.IsNullOrWhiteSpace(_startupCommand)) + { + return false; + } + + try + { + using var runKey = Registry.CurrentUser.CreateSubKey(RunKeyPath); + if (runKey is null) + { + return false; + } + + if (enabled) + { + runKey.SetValue(ValueName, _startupCommand, RegistryValueKind.String); + } + else + { + runKey.DeleteValue(ValueName, throwOnMissingValue: false); + } + + return IsEnabled() == enabled; + } + catch (Exception ex) + { + Logger.Warn($"LauncherWindowsStartup: failed to set Run key. Enabled={enabled}. {ex.Message}"); + return false; + } + } +} diff --git a/LanMountainDesktop.Launcher/Views/OobeWindow.axaml b/LanMountainDesktop.Launcher/Views/OobeWindow.axaml index eb55cfa..e098665 100644 --- a/LanMountainDesktop.Launcher/Views/OobeWindow.axaml +++ b/LanMountainDesktop.Launcher/Views/OobeWindow.axaml @@ -596,7 +596,142 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +