mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
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.
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 在 OOBE 中向 Host 的 settings.json 写入启动与展示相关字段,属性名与 Host
|
||||
/// AppSettingsSnapshot 的 JSON 序列化一致(PascalCase)。
|
||||
/// </summary>
|
||||
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<bool>(out var b) => b,
|
||||
JsonValue v when v.TryGetValue<string>(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);
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace LanMountainDesktop.Launcher.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 将当前 Windows 用户登录时自启动项指向<strong>本 Launcher 进程</strong>(与正式入口一致)。
|
||||
/// Host 内 WindowsStartupService 使用 Host 进程路径;
|
||||
/// OOBE 在 Launcher 内执行时应使用本类型,以便开机后仍走更新/版本协调流程。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -596,7 +596,142 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 步骤 4: 信息与隐私页面 -->
|
||||
<!-- 步骤 4: 启动与展示(紧接数据保存位置) -->
|
||||
<Grid x:Name="StartupPresentationStep" Margin="48" RowDefinitions="Auto,*,Auto" IsVisible="False">
|
||||
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,16">
|
||||
<TextBlock Text="启动与展示"
|
||||
FontSize="24"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="这些选项可随时在桌面应用的「设置」中更改。主窗口滑动入场仅在 Windows 上可用。"
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="14">
|
||||
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
|
||||
Padding="16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="在任务栏显示主桌面窗口"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="开启后最小化时可在任务栏保留条目;关闭则更多依赖托盘图标。"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch x:Name="OobeShowInTaskbarToggle"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="OobeSlideTransitionSection"
|
||||
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
|
||||
Padding="16">
|
||||
<StackPanel Spacing="12">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="以滑动方式显示主窗口"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="自屏幕边缘滑入;与「淡入」二选一。"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch x:Name="OobeSlideTransitionToggle"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="启动时使用淡入过渡"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="在未启用滑动入场时建议使用。"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch x:Name="OobeFadeTransitionToggle"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
|
||||
Padding="16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<fi:SymbolIcon Symbol="SlideLayout"
|
||||
FontSize="20"
|
||||
Foreground="{DynamicResource AccentFillColorDefaultBrush}" />
|
||||
<TextBlock Text="融合桌面与弹入手势"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="同时启用融合桌面与三指滑动手势,以便使用边缘弹入与相关实验特性(与设置中开发者选项一致)。"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch x:Name="OobeFusedPopupToggle"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
|
||||
Padding="16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="登录 Windows 时自动启动阑山桌面"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock x:Name="OobeAutoStartDescriptionText"
|
||||
Text="通过当前用户的启动项注册本启动器(与安装程序可选任务使用同一注册表项)。"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch x:Name="OobeAutoStartToggle"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="12"
|
||||
Margin="0,24,0,0">
|
||||
<Button x:Name="StartupPresentationBackButton"
|
||||
Content="返回"
|
||||
Theme="{DynamicResource ButtonTheme}" />
|
||||
<Button x:Name="StartupPresentationNextButton"
|
||||
Content="下一步"
|
||||
Theme="{DynamicResource AccentButtonTheme}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 步骤 5: 信息与隐私页面 -->
|
||||
<Grid x:Name="PrivacyStep" Margin="48" RowDefinitions="Auto,*,Auto" IsVisible="False">
|
||||
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,24">
|
||||
<TextBlock Text="信息与隐私"
|
||||
@@ -731,7 +866,7 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 步骤 5: 欢迎完成页面 -->
|
||||
<!-- 步骤 6: 欢迎完成页面 -->
|
||||
<Grid x:Name="WelcomeStep" Margin="48" RowDefinitions="*,Auto" IsVisible="False">
|
||||
<StackPanel Grid.Row="0"
|
||||
VerticalAlignment="Center"
|
||||
|
||||
@@ -31,6 +31,9 @@ public partial class OobeWindow : Window
|
||||
private string _selectedAccentColor = "#0078D4";
|
||||
private MonetSource _selectedMonetSource = MonetSource.Wallpaper;
|
||||
|
||||
private readonly bool _startupSlideUiAvailable;
|
||||
private bool _suppressOobeStartupTransitionHandlers;
|
||||
|
||||
public OobeWindow()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
@@ -39,6 +42,7 @@ public partial class OobeWindow : Window
|
||||
|
||||
var appRoot = AppDomain.CurrentDomain.BaseDirectory;
|
||||
_resolver = new DataLocationResolver(appRoot);
|
||||
_startupSlideUiAvailable = OperatingSystem.IsWindows();
|
||||
}
|
||||
|
||||
public void SetDebugMode(bool isDebugMode)
|
||||
@@ -181,7 +185,27 @@ public partial class OobeWindow : Window
|
||||
};
|
||||
}
|
||||
|
||||
// 步骤 4: 隐私设置页面
|
||||
if (this.FindControl<Button>("StartupPresentationBackButton") is { } startupPresentationBack)
|
||||
{
|
||||
startupPresentationBack.Click += OnStartupPresentationBackClick;
|
||||
}
|
||||
|
||||
if (this.FindControl<Button>("StartupPresentationNextButton") is { } startupPresentationNext)
|
||||
{
|
||||
startupPresentationNext.Click += OnStartupPresentationNextClick;
|
||||
}
|
||||
|
||||
if (this.FindControl<ToggleSwitch>("OobeSlideTransitionToggle") is { } oobeSlideTransition)
|
||||
{
|
||||
oobeSlideTransition.IsCheckedChanged += OnOobeStartupSlideTransitionChanged;
|
||||
}
|
||||
|
||||
if (this.FindControl<ToggleSwitch>("OobeFadeTransitionToggle") is { } oobeFadeTransition)
|
||||
{
|
||||
oobeFadeTransition.IsCheckedChanged += OnOobeStartupFadeTransitionChanged;
|
||||
}
|
||||
|
||||
// 步骤 5: 隐私设置页面
|
||||
if (this.FindControl<Button>("PrivacyBackButton") is { } privacyBackButton)
|
||||
{
|
||||
privacyBackButton.Click += OnPrivacyBackClick;
|
||||
@@ -203,7 +227,7 @@ public partial class OobeWindow : Window
|
||||
privacyCheckBox.IsCheckedChanged += OnPrivacyAgreementChanged;
|
||||
}
|
||||
|
||||
// 步骤 5: 欢迎完成页面
|
||||
// 步骤 6: 欢迎完成页面
|
||||
if (this.FindControl<Button>("EnterButton") is { } enterButton)
|
||||
{
|
||||
enterButton.Click += OnEnterClick;
|
||||
@@ -460,11 +484,189 @@ public partial class OobeWindow : Window
|
||||
await NavigateToStep(4);
|
||||
}
|
||||
|
||||
// 启动与展示(OOBE 步骤 4)
|
||||
private async void OnStartupPresentationBackClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_isTransitioning) return;
|
||||
await NavigateToStep(3);
|
||||
}
|
||||
|
||||
private async void OnStartupPresentationNextClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_isTransitioning) return;
|
||||
SaveOobeStartupPresentation();
|
||||
await NavigateToStep(5);
|
||||
}
|
||||
|
||||
private void SaveOobeStartupPresentation()
|
||||
{
|
||||
try
|
||||
{
|
||||
var choices = CollectOobeStartupChoices();
|
||||
var path = HostAppSettingsOobeMerger.GetSettingsFilePath(_resolver.ResolveDataRoot());
|
||||
HostAppSettingsOobeMerger.MergeStartupPresentation(path, choices);
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_ = new LauncherWindowsStartupService().SetEnabled(choices.AutoStartWithWindows);
|
||||
}
|
||||
|
||||
Logger.Info($"[OobeWindow] 启动与展示已写入 '{path}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"[OobeWindow] 启动与展示保存失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshOobeStartupPresentationFromDisk()
|
||||
{
|
||||
var path = HostAppSettingsOobeMerger.GetSettingsFilePath(_resolver.ResolveDataRoot());
|
||||
var defaults = HostAppSettingsOobeMerger.LoadStartupDefaults(path);
|
||||
|
||||
if (this.FindControl<Border>("OobeSlideTransitionSection") is { } slideSection)
|
||||
{
|
||||
slideSection.IsVisible = _startupSlideUiAvailable;
|
||||
}
|
||||
|
||||
if (this.FindControl<TextBlock>("OobeAutoStartDescriptionText") is { } autoStartDesc)
|
||||
{
|
||||
autoStartDesc.Text = OperatingSystem.IsWindows()
|
||||
? "通过当前用户的启动项注册本启动器(与安装程序可选任务使用同一注册表项)。"
|
||||
: "当前平台仅保存偏好;是否随系统自启动请使用系统提供的应用自启动设置。";
|
||||
}
|
||||
|
||||
_suppressOobeStartupTransitionHandlers = true;
|
||||
try
|
||||
{
|
||||
if (this.FindControl<ToggleSwitch>("OobeShowInTaskbarToggle") is { } taskbar)
|
||||
{
|
||||
taskbar.IsChecked = defaults.ShowInTaskbar;
|
||||
}
|
||||
|
||||
if (_startupSlideUiAvailable)
|
||||
{
|
||||
if (this.FindControl<ToggleSwitch>("OobeSlideTransitionToggle") is { } slide)
|
||||
{
|
||||
slide.IsChecked = defaults.EnableSlideTransition;
|
||||
}
|
||||
|
||||
if (this.FindControl<ToggleSwitch>("OobeFadeTransitionToggle") is { } fade)
|
||||
{
|
||||
fade.IsChecked = defaults.EnableFadeTransition;
|
||||
fade.IsEnabled = !defaults.EnableSlideTransition;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.FindControl<ToggleSwitch>("OobeFusedPopupToggle") is { } fused)
|
||||
{
|
||||
fused.IsChecked = defaults.FusedPopupExperience;
|
||||
}
|
||||
|
||||
if (this.FindControl<ToggleSwitch>("OobeAutoStartToggle") is { } autoStart)
|
||||
{
|
||||
autoStart.IsChecked = defaults.AutoStartWithWindows;
|
||||
autoStart.IsEnabled = OperatingSystem.IsWindows();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressOobeStartupTransitionHandlers = false;
|
||||
}
|
||||
}
|
||||
|
||||
private HostAppSettingsStartupChoices CollectOobeStartupChoices()
|
||||
{
|
||||
var showTaskbar = this.FindControl<ToggleSwitch>("OobeShowInTaskbarToggle")?.IsChecked == true;
|
||||
var fused = this.FindControl<ToggleSwitch>("OobeFusedPopupToggle")?.IsChecked == true;
|
||||
var autoStart = OperatingSystem.IsWindows() &&
|
||||
this.FindControl<ToggleSwitch>("OobeAutoStartToggle")?.IsChecked == true;
|
||||
|
||||
bool fade;
|
||||
bool slide;
|
||||
if (_startupSlideUiAvailable)
|
||||
{
|
||||
slide = this.FindControl<ToggleSwitch>("OobeSlideTransitionToggle")?.IsChecked == true;
|
||||
fade = this.FindControl<ToggleSwitch>("OobeFadeTransitionToggle")?.IsChecked == true;
|
||||
}
|
||||
else
|
||||
{
|
||||
slide = false;
|
||||
fade = true;
|
||||
}
|
||||
|
||||
return new HostAppSettingsStartupChoices(
|
||||
ShowInTaskbar: showTaskbar,
|
||||
EnableFadeTransition: fade,
|
||||
EnableSlideTransition: slide,
|
||||
FusedPopupExperience: fused,
|
||||
AutoStartWithWindows: autoStart);
|
||||
}
|
||||
|
||||
private void OnOobeStartupSlideTransitionChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!_startupSlideUiAvailable || _suppressOobeStartupTransitionHandlers)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender is not ToggleSwitch slide || slide.IsChecked != true)
|
||||
{
|
||||
if (this.FindControl<ToggleSwitch>("OobeFadeTransitionToggle") is { } fade)
|
||||
{
|
||||
fade.IsEnabled = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_suppressOobeStartupTransitionHandlers = true;
|
||||
try
|
||||
{
|
||||
if (this.FindControl<ToggleSwitch>("OobeFadeTransitionToggle") is { } fade)
|
||||
{
|
||||
fade.IsChecked = false;
|
||||
fade.IsEnabled = false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressOobeStartupTransitionHandlers = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOobeStartupFadeTransitionChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!_startupSlideUiAvailable || _suppressOobeStartupTransitionHandlers)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender is not ToggleSwitch fade || fade.IsChecked != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_suppressOobeStartupTransitionHandlers = true;
|
||||
try
|
||||
{
|
||||
if (this.FindControl<ToggleSwitch>("OobeSlideTransitionToggle") is { } slide)
|
||||
{
|
||||
slide.IsChecked = false;
|
||||
}
|
||||
|
||||
fade.IsEnabled = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressOobeStartupTransitionHandlers = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 隐私设置页面按钮
|
||||
private async void OnPrivacyBackClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_isTransitioning) return;
|
||||
await NavigateToStep(3);
|
||||
await NavigateToStep(4);
|
||||
}
|
||||
|
||||
private async void OnPrivacyNextClick(object? sender, RoutedEventArgs e)
|
||||
@@ -474,7 +676,7 @@ public partial class OobeWindow : Window
|
||||
// 保存隐私设置
|
||||
SavePrivacySettings();
|
||||
|
||||
await NavigateToStep(5);
|
||||
await NavigateToStep(6);
|
||||
}
|
||||
|
||||
private void OnViewPrivacyPolicyClick(object? sender, RoutedEventArgs e)
|
||||
@@ -712,8 +914,9 @@ public partial class OobeWindow : Window
|
||||
1 => this.FindControl<Grid>("TypingStep"),
|
||||
2 => this.FindControl<Grid>("ThemeStep"),
|
||||
3 => this.FindControl<Grid>("DataLocationStep"),
|
||||
4 => this.FindControl<Grid>("PrivacyStep"),
|
||||
5 => this.FindControl<Grid>("WelcomeStep"),
|
||||
4 => this.FindControl<Grid>("StartupPresentationStep"),
|
||||
5 => this.FindControl<Grid>("PrivacyStep"),
|
||||
6 => this.FindControl<Grid>("WelcomeStep"),
|
||||
_ => null
|
||||
};
|
||||
|
||||
@@ -723,8 +926,9 @@ public partial class OobeWindow : Window
|
||||
1 => this.FindControl<Grid>("TypingStep"),
|
||||
2 => this.FindControl<Grid>("ThemeStep"),
|
||||
3 => this.FindControl<Grid>("DataLocationStep"),
|
||||
4 => this.FindControl<Grid>("PrivacyStep"),
|
||||
5 => this.FindControl<Grid>("WelcomeStep"),
|
||||
4 => this.FindControl<Grid>("StartupPresentationStep"),
|
||||
5 => this.FindControl<Grid>("PrivacyStep"),
|
||||
6 => this.FindControl<Grid>("WelcomeStep"),
|
||||
_ => null
|
||||
};
|
||||
|
||||
@@ -737,6 +941,11 @@ public partial class OobeWindow : Window
|
||||
await AnimateOpacityAsync(currentStepControl, 1, 0, AnimationDurationMs);
|
||||
currentStepControl.IsVisible = false;
|
||||
|
||||
if (step == 4)
|
||||
{
|
||||
RefreshOobeStartupPresentationFromDisk();
|
||||
}
|
||||
|
||||
nextStepControl.IsVisible = true;
|
||||
nextStepControl.Opacity = 0;
|
||||
await AnimateOpacityAsync(nextStepControl, 0, 1, AnimationDurationMs);
|
||||
|
||||
91
LanMountainDesktop.Tests/HostAppSettingsOobeMergerTests.cs
Normal file
91
LanMountainDesktop.Tests/HostAppSettingsOobeMergerTests.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using LanMountainDesktop.Launcher.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class HostAppSettingsOobeMergerTests
|
||||
{
|
||||
[Fact]
|
||||
public void MergeStartupPresentation_PreservesUnrelatedJsonKeys()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "LMD.OobeMerge", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var path = Path.Combine(dir, "settings.json");
|
||||
File.WriteAllText(path, """
|
||||
{
|
||||
"LanguageCode": "ja-JP",
|
||||
"ShowInTaskbar": false,
|
||||
"EnableFadeTransition": true,
|
||||
"EnableSlideTransition": false
|
||||
}
|
||||
""");
|
||||
|
||||
try
|
||||
{
|
||||
HostAppSettingsOobeMerger.MergeStartupPresentation(
|
||||
path,
|
||||
new HostAppSettingsStartupChoices(
|
||||
ShowInTaskbar: true,
|
||||
EnableFadeTransition: false,
|
||||
EnableSlideTransition: true,
|
||||
FusedPopupExperience: true,
|
||||
AutoStartWithWindows: true));
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
Assert.Equal("ja-JP", root.GetProperty("LanguageCode").GetString());
|
||||
Assert.True(root.GetProperty("ShowInTaskbar").GetBoolean());
|
||||
Assert.False(root.GetProperty("EnableFadeTransition").GetBoolean());
|
||||
Assert.True(root.GetProperty("EnableSlideTransition").GetBoolean());
|
||||
Assert.True(root.GetProperty("EnableFusedDesktop").GetBoolean());
|
||||
Assert.True(root.GetProperty("EnableThreeFingerSwipe").GetBoolean());
|
||||
Assert.True(root.GetProperty("AutoStartWithWindows").GetBoolean());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSettingsFilePath_NormalizesDataRoot()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "LMD.OobePath", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
var path = HostAppSettingsOobeMerger.GetSettingsFilePath(root + Path.DirectorySeparatorChar);
|
||||
Assert.Equal(Path.Combine(Path.GetFullPath(root), "settings.json"), path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadStartupDefaults_WhenFusedAndSwipeDiffer_TreatsPopupExperienceAsBothTrue()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "LMD.OobeDefaults", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var path = Path.Combine(dir, "settings.json");
|
||||
File.WriteAllText(path, """
|
||||
{
|
||||
"EnableFusedDesktop": true,
|
||||
"EnableThreeFingerSwipe": false
|
||||
}
|
||||
""");
|
||||
|
||||
try
|
||||
{
|
||||
var d = HostAppSettingsOobeMerger.LoadStartupDefaults(path);
|
||||
Assert.False(d.FusedPopupExperience);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,9 @@
|
||||
OpenPaneLength="283"
|
||||
IsSettingsVisible="False"
|
||||
IsBackButtonVisible="False"
|
||||
SelectionChanged="OnNavigationSelectionChanged">
|
||||
IsPaneToggleButtonVisible="False"
|
||||
SelectionChanged="OnNavigationSelectionChanged"
|
||||
ItemInvoked="OnNavigationItemInvoked">
|
||||
<ui:FANavigationView.Styles>
|
||||
<Style Selector="ui|FANavigationView#RootNavigationView:minimal">
|
||||
<Setter Property="IsPaneToggleButtonVisible" Value="False" />
|
||||
@@ -178,20 +180,6 @@
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
</Button>
|
||||
|
||||
<Button x:Name="TitleBarPaneToggleButton"
|
||||
Classes="titlebar-icon-button"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="0,-8,-8,-8"
|
||||
ToolTip.Tip="{Binding TogglePaneTooltip}"
|
||||
Click="OnTitleBarPaneToggleClick">
|
||||
<fi:FluentIcon x:Name="TitleBarPaneToggleButtonIcon"
|
||||
Icon="Navigation"
|
||||
IconVariant="Regular"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
</Button>
|
||||
|
||||
<fi:FluentIcon x:Name="WindowBrandIcon"
|
||||
Icon="Settings"
|
||||
IconVariant="Filled"
|
||||
|
||||
@@ -31,6 +31,7 @@ public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext
|
||||
private const double MinPaneOpenLength = 260d;
|
||||
private const double MaxPaneOpenLength = 288d;
|
||||
private const double BaseNarrowThreshold = 800d;
|
||||
private const string PaneToggleItemTag = "__pane_toggle__";
|
||||
|
||||
private readonly ISettingsPageRegistry _pageRegistry;
|
||||
private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
|
||||
@@ -42,6 +43,7 @@ public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext
|
||||
private bool _isResponsiveRefreshPending;
|
||||
private bool _isRestartPromptVisible;
|
||||
private bool _isHandlingSearchSelection;
|
||||
private FANavigationViewItem? _paneToggleItem;
|
||||
private Border? _currentSearchHighlight;
|
||||
private Action? _searchHighlightCleanup;
|
||||
|
||||
@@ -96,7 +98,6 @@ public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext
|
||||
SyncTitleText();
|
||||
UpdateChromeMetrics();
|
||||
UpdatePaneToggleVisibility();
|
||||
UpdatePaneToggleIcon();
|
||||
UpdateResponsiveLayout();
|
||||
RequestResponsiveLayoutRefresh();
|
||||
}
|
||||
@@ -216,6 +217,16 @@ public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext
|
||||
RootNavigationView.MenuItems.Clear();
|
||||
RootNavigationView.FooterMenuItems.Clear();
|
||||
|
||||
_paneToggleItem = new FANavigationViewItem
|
||||
{
|
||||
Content = string.Empty,
|
||||
Tag = PaneToggleItemTag,
|
||||
IconSource = CreateSettingsIconSource(Symbol.Navigation),
|
||||
SelectsOnInvoked = false
|
||||
};
|
||||
ToolTip.SetTip(_paneToggleItem, ViewModel.TogglePaneTooltip);
|
||||
RootNavigationView.MenuItems.Add(_paneToggleItem);
|
||||
|
||||
SettingsPageCategory? previousCategory = null;
|
||||
|
||||
foreach (var page in ViewModel.Pages)
|
||||
@@ -248,10 +259,29 @@ public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext
|
||||
private void OnNavigationSelectionChanged(object? sender, FANavigationViewSelectionChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
var selectedItem = e.SelectedItemContainer ?? e.SelectedItem as FANavigationViewItem;
|
||||
var selectedItem = e.SelectedItemContainer ?? e.SelectedItem as Control;
|
||||
if (IsPaneToggleItem(selectedItem))
|
||||
{
|
||||
RestoreCurrentNavigationSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
NavigateTo(selectedItem?.Tag as string, addHistory: true, source: "navigation");
|
||||
}
|
||||
|
||||
private void OnNavigationItemInvoked(object? sender, FANavigationViewItemInvokedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
var invokedItem = e.InvokedItemContainer ?? e.InvokedItem as Control;
|
||||
if (!IsPaneToggleItem(invokedItem))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ToggleNavigationPane();
|
||||
RestoreCurrentNavigationSelection();
|
||||
}
|
||||
|
||||
private void NavigateTo(
|
||||
string? pageId,
|
||||
bool addHistory,
|
||||
@@ -803,17 +833,14 @@ public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnTitleBarPaneToggleClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
private void ToggleNavigationPane()
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (RootNavigationView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RootNavigationView.IsPaneOpen = !RootNavigationView.IsPaneOpen;
|
||||
UpdatePaneToggleIcon();
|
||||
UpdateResponsiveLayout();
|
||||
RequestResponsiveLayoutRefresh();
|
||||
}
|
||||
@@ -832,23 +859,35 @@ public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext
|
||||
UpdatePaneToggleVisibility();
|
||||
}
|
||||
|
||||
UpdatePaneToggleIcon();
|
||||
RequestResponsiveLayoutRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅在 <c>:minimal</c>(<see cref="FANavigationView.IsPaneToggleButtonVisible"/> 为 false)时显示侧栏底部备胎按钮。
|
||||
/// 根 DataContext 为 ViewModel 时,对 <c>#RootNavigationView</c> 的绑定易失效,故用代码同步可见性。
|
||||
/// The NavigationView template toggle is disabled; the first menu item is the only pane toggle.
|
||||
/// </summary>
|
||||
private void UpdatePaneToggleVisibility()
|
||||
{
|
||||
if (TitleBarPaneToggleButton is null || RootNavigationView is null)
|
||||
if (_paneToggleItem is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TitleBarPaneToggleButton.IsVisible = !RootNavigationView.IsPaneToggleButtonVisible;
|
||||
_paneToggleItem.IsVisible = true;
|
||||
ToolTip.SetTip(_paneToggleItem, ViewModel.TogglePaneTooltip);
|
||||
}
|
||||
|
||||
private static bool IsPaneToggleItem(Control? item)
|
||||
{
|
||||
return string.Equals(item?.Tag as string, PaneToggleItemTag, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private void RestoreCurrentNavigationSelection()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ViewModel.CurrentPageId))
|
||||
{
|
||||
TrySelectNavigationItem(ViewModel.CurrentPageId);
|
||||
}
|
||||
}
|
||||
|
||||
private void RequestResponsiveLayoutRefresh()
|
||||
@@ -880,18 +919,6 @@ public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext
|
||||
: compactPaneWidth;
|
||||
}
|
||||
|
||||
private void UpdatePaneToggleIcon()
|
||||
{
|
||||
if (TitleBarPaneToggleButtonIcon is null || RootNavigationView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TitleBarPaneToggleButtonIcon.Icon = RootNavigationView.IsPaneOpen
|
||||
? FluentIcons.Common.Icon.LineHorizontal3
|
||||
: FluentIcons.Common.Icon.Navigation;
|
||||
}
|
||||
|
||||
private void UpdateChromeMetrics()
|
||||
{
|
||||
if (_useSystemChrome)
|
||||
|
||||
@@ -531,6 +531,8 @@ _oobeSteps = [
|
||||
];
|
||||
```
|
||||
|
||||
当前内置 OOBE 向导窗口(`OobeWindow`)内步骤顺序包含:开场 → 主题 → **数据保存位置** → **启动与展示** → 隐私与遥测 → 完成。「启动与展示」写入 Host 的 `settings.json`(PascalCase)并在 Windows 下同步 Run 项,实现代码在 `HostAppSettingsOobeMerger.cs` 与 `LauncherWindowsStartupService.cs`,界面与逻辑挂在 `Views/OobeWindow.axaml(.cs)`。
|
||||
|
||||
### 自定义更新源
|
||||
|
||||
修改 `App.axaml.cs` 中的 GitHub 仓库信息:
|
||||
|
||||
Reference in New Issue
Block a user