mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
* Add Windows system chrome patchers (Harmony) Introduce support for toggling the system chrome on Windows using Harmony patchers. Adds Lib.Harmony.Thin to package props and project, new patcher infrastructure (ChromePatchState, PatcherEntrance) and two Harmony patches that disable FluentAvalonia's Windows chrome when configured. Program.cs now loads the chrome setting and installs patchers conditionally on Windows/x86-x64. Settings viewmodel and view updated: expose IsWindowsOs, require restart on appearance changes, migrate SettingsWindow to FAAppWindow and adapt titlebar/layout (include Windows caption placeholder and footer menu items). Also add a .gitkeep and a build log file. * Refactor settings window UI and theming Improve theming and layout for the Settings window and related services. - MaterialSurfaceService: add special material parameters for SettingsWindowBackground (lower alpha, no blur) and avoid hot-switching real backdrops for non-settings windows. - GlassEffectService: add AdaptiveSettingsWindowTintBrush + ResolveSettingsWindowTintAlpha to provide optional content tinting tied to system material mode. - SettingsWindowService: refactor theme application into ApplyThemeVariantAndResources, ensure settings window material is applied at show/activate times, and tidy theme/resource application flow. - SettingsWindow.axaml / .axaml.cs: restructure title bar (separate Grid.Row=0 border) and FANavigationView host, add pane-footer toggle button for :minimal layout, use dynamic corner radius resource, and update toggle/visibility/icon logic and responsive layout code. - SettingsPages: remove some IconText usages and adjust margins; use DesignCornerRadiusLg for update card corner radius. - Add NuGet.Config to set local globalPackagesFolder and ignore .nuget/packages in .gitignore. These changes aim to improve visuals, avoid backdrop overdraw, and make the settings window behavior consistent across themes and layouts. * Add localization and localize settings pages Add many new localization keys (en-US and zh-CN) for notifications, developer tools, about page, status bar, and video wallpaper. Update Notification, Dev, About and StatusBar view models to use LocalizationService, expose localized ObservableProperties, and refresh localized text at construction. Localize selection options and test notification texts, and fix notification severity handling. Wire up XAML to the new localized properties (About/Dev/StatusBar pages) and update the settings page title for notifications. Also adjust copyright line generation and replace hardcoded placeholders with bound Watermark properties. * Redesign settings window with fluent shell & search Rebuild the settings window as a Fluent shell: adds a custom 48-DIP titlebar with Back, pane toggle, icon/title, search box, restart/more menu, and caption-button spacer; moves compact pane toggle into the titlebar and preserves FANavigationView as the primary navigation surface. Introduces a SettingsSearchService (with UI AutoComplete integration, search indexing, navigation-by-result, and search result highlighting) plus focused tests for search filtering and theme material normalization. Adds navigation history/back stack, updates SettingsViewModels for new bindings and localization keys, and updates General/Apearance pages to expose new strings and options. Implements an "auto" system material mode: default in AppSettingsSnapshot, new MaterialAuto constants and normalization/resolution logic in ThemeAppearanceValues, WindowMaterialService and MaterialSurfaceService adjustments to prefer Mica on Win11 and Acrylic on Win10 using TransparencyLevelHint. GlassEffectService and AppearanceThemeService updated to use effective material mode and to track live theme state changes. Adds localization entries (en-US, zh-CN), spec/tasks docs, and other UI/style tweaks to support the redesign. * fix.修折叠与展开按钮 * 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. * Move whiteboard persistence to file storage Switch whiteboard note storage from legacy DB rows to per-note JSON files and add migration support. Update WhiteboardNoteSnapshot schema (version bump, viewport, canvas, expires, PathSvgData) and change IWhiteboardNotePersistenceService.SaveNote to return bool to surface write failures (e.g. read-only files). Implement file-based WhiteboardNotePersistenceService with legacy DB migration/cleanup, retention handling, and logging. Add comprehensive unit tests for persistence, stroke path builder, SVG import and viewport helper. Also add ThirdParty/DotNetCampus.InkCanvas project and reference it in the main csproj, and bump PostHog package to 2.6.0. * Introduce render gate and chart caching Replace UI DispatcherTimer polling with a StudySnapshotRenderGate across multiple widgets to queue and apply only the latest analytics snapshot; components updated include StudyDeductionReasonsWidget, StudyEnvironmentWidget, StudyInterruptDensityWidget, StudyNoiseCurveWidget. Add StudySnapshotRenderGate implementation to coordinate rendering and monitoring leases and update subscription/lease lifecycle handling (subscribe/unsubscribe, Acquire/Dispose leases, Clear/Dispose gate). Rewrite chart controls (StudyNoiseCurveChartControl and StudyNoiseDistributionScatterChartControl) to use stable logical-time origins, split series into static vs dynamic tails, add geometry/sample caching, stable jitter/coordinate mapping helpers, and expose internal helpers & counts for testing. Add unit tests (StudyComponentRenderingTests) covering the render gate and chart behaviors (layer counts, logical X mapping, stable jitter, cache rebuild). These changes improve rendering correctness and performance by avoiding redundant renders and enabling deterministic chart layout. * Use MaterialColorSnapshot in appearance flow Introduce unified material/color spec and tests, and refactor appearance plumbing to use MaterialColorSnapshot as the single source of truth. Add .trae material-color-service spec/checklist/tasks and integration/unit tests for plugin mapping and appearance VM behavior. AppearanceChangedEvent extended with new appearance change flags and HasChanged logic. ComponentEditorMaterialThemeAdapter rewritten to accept MaterialColorSnapshot and derive palette from snapshot data. Simplify AppearanceSettingsPageViewModel and related view code: remove legacy preview/custom-seed UI logic, preserve material/color fields when updating theme or corner radius, and update save calls to use with-expressions. Update ComponentEditorWindow to use adapter-provided OnPrimary brush and minor docs updates. * Add material color services, plugin DTOs, and tests Introduce IPC wire-format appearance DTOs (PluginIsolation.Contracts) and clarify they are distinct from the runtime PluginSdk snapshot. Update PluginSdk comments to document the runtime-facing snapshot shape. Change ComponentColorSchemeHelper to use the HostMaterialColorProvider and add an overload that accepts a MaterialColorSnapshot. Add new services and pipelines (MaterialColorService, MaterialSurfaceService, WindowMaterialService, WallpaperColorPipeline) and refactor AppearanceThemeService to depend on MaterialColorService while removing legacy internal implementations. Add multiple unit tests (ComponentColorSchemeHelper, PluginAppearanceBoundary, SettingsCatalogService, WallpaperSettingsPageViewModel) and update localization resources with new material_color and wallpaper keys. * Add CODE_WIKI and update localization Add a comprehensive CODE_WIKI.md documenting project architecture, modules, startup flow, plugin system, testing and developer workflows. Update localization resources (en-US.json, zh-CN.json) with new/translated keys for wallpaper controls (custom color UI), material & color settings (semantic roles, surfaces, refresh/polling state), appearance (corner radius), status bar font size options, privacy policy text, component library labels, clock settings, and new language entry (Korean). Also modify settings-related ViewModels and Settings page views to surface these new features and texts (MaterialColorSettingsPageViewModel.cs, SettingsViewModels.cs, WallpaperSettingsPageViewModel.cs, MainWindow.SettingsHardCut.Stubs.cs, ComponentsSettingsPage.axaml, WallpaperSettingsPage.axaml). * Add Data settings page and storage scanner Introduce a new "Data" settings page to visualize and manage local app storage. Adds DataStorageService (scanning, disk info, clean operations), DataSettingsPageViewModel, XAML view and code-behind, and HexToColor/HexToBrush converters; registers converters in App.axaml. Also update localization strings for the new page and add icon mapping so the settings entry uses the Database icon. Enables per-category and global cleaning workflows and formatted size display. * Add IPC backoff/retries and safer disposal Introduce exponential backoff, jitter and retry logic across IPC components to improve robustness and avoid tight retry loops; make disposal idempotent and add connection guards. Key changes: - LauncherCoordinatorIpcServer / LauncherIpcServer: add backoff constants, ComputeBackoff(), consecutive error tracking and delayed retries with jitter. - LanMountainDesktopIpcClient / LauncherIpcClient: add connect retry loops, timeouts, delayed retries, improved error logging, and use ArrayPool for buffered async writes; ensure proper cleanup on failures. - PublicIpcHostService: add disposed flag, guard OnPeerConnected and Dispose, and clear connected peers on dispose. - Add many auto-generated commit analysis docs under docs/auto_commit_md and new scripts for analyzing/generating commit docs. These changes aim to make IPC connection handling more resilient and resource-safe. * Add preview controls and settings UI tweaks Introduce GridPreviewControl and CornerRadiusPreviewControl for visual previews and wire them into the Components settings (add ScreenAspectRatio, CornerRadiusPreviewValue, and screen aspect init). Refactor ComponentsSettingsPage UI to show live previews. Improve DataSettingsPage layout and storage bar logic (use item percentages directly, include remaining segment, adjust visuals and visibility triggers). Simplify LauncherSettingsPage header/appearance layout. Add SECURITY_AUDIT_REPORT.md, analysis summary, mockup HTML, and a local .claude settings file. * Add install checkpoint/resume and DDSS workflows Introduce install checkpoint support and resume logic for updates, plus related locking and validation. Adds InstallCheckpoint model, AppJsonContext serialization, and UpdatePaths helpers for deployment lock, apply-in-progress lock and install-checkpoint path. UpdateEngineService gains checkpoint load/save/delete, incoming-state validation, resume logic for PLONDS and legacy updates, apply lock handling, and safer cleanup; ApplyPendingPlondsUpdateAsync and ApplyPendingUpdate flow updated accordingly. Add DeploymentLock contract and extend UpdateState with pause/resume/cancel helpers. Tests updated to cover stale/valid checkpoint resume and legacy/PLONDS flows. CI: enhance ddss-publish to detect release channel, validate S3 assets, prepare and atomically publish channel pointer; add ddss-rollback workflow to publish rollbacks; adjust plonds-build concurrency and release events. * changed.更了好多 * fix.消息盒子媒体播放器组件服务修复 * change.重做天气,为回到系统提供自定义功能。 * feat.airapp与融合桌面 * feat.动画优化与更新界面 * feat.数字时钟,白板功能修复 * feat.完善了时钟轻应用,为启动器提供了多语言支持 * feat.发布与打包优化 * changed.天气选项卡更新
1069 lines
36 KiB
C#
1069 lines
36 KiB
C#
using Avalonia;
|
||
using Avalonia.Animation;
|
||
using Avalonia.Controls;
|
||
using Avalonia.Input;
|
||
using Avalonia.Interactivity;
|
||
using Avalonia.Markup.Xaml;
|
||
using Avalonia.Media;
|
||
using Avalonia.Threading;
|
||
using LanMountainDesktop.Launcher.Models;
|
||
using LanMountainDesktop.Launcher.Resources;
|
||
using LanMountainDesktop.Launcher.Services;
|
||
|
||
namespace LanMountainDesktop.Launcher.Views;
|
||
|
||
public partial class OobeWindow : Window
|
||
{
|
||
private const int AnimationDurationMs = 300;
|
||
private const int TypingDelayMs = 100;
|
||
|
||
private readonly TaskCompletionSource<bool> _completionSource = new();
|
||
private readonly DataLocationResolver _resolver;
|
||
private bool _isTransitioning;
|
||
private bool _isDebugMode;
|
||
private int _currentStep = 1;
|
||
|
||
// 数据位置选择
|
||
private DataLocationMode _selectedDataLocationMode = DataLocationMode.System;
|
||
private bool _migrateExistingData;
|
||
|
||
// 主题选择
|
||
private Services.ThemeMode _selectedThemeMode = Services.ThemeMode.Light;
|
||
private string _selectedAccentColor = "#0078D4";
|
||
private MonetSource _selectedMonetSource = MonetSource.Wallpaper;
|
||
|
||
private readonly bool _startupSlideUiAvailable;
|
||
private bool _suppressOobeStartupTransitionHandlers;
|
||
|
||
public OobeWindow()
|
||
{
|
||
AvaloniaXamlLoader.Load(this);
|
||
Loaded += OnWindowLoaded;
|
||
Opened += OnWindowOpened;
|
||
|
||
var appRoot = AppDomain.CurrentDomain.BaseDirectory;
|
||
_resolver = new DataLocationResolver(appRoot);
|
||
_startupSlideUiAvailable = OperatingSystem.IsWindows();
|
||
}
|
||
|
||
public void SetDebugMode(bool isDebugMode)
|
||
{
|
||
_isDebugMode = isDebugMode;
|
||
}
|
||
|
||
public Task WaitForEnterAsync() => _completionSource.Task;
|
||
|
||
private void OnWindowLoaded(object? sender, RoutedEventArgs e)
|
||
{
|
||
InitializeDataLocationStep();
|
||
InitializePrivacySettings();
|
||
SetupEventHandlers();
|
||
}
|
||
|
||
private void SetupEventHandlers()
|
||
{
|
||
// 步骤 1: 开始按钮
|
||
if (this.FindControl<Button>("StartButton") is { } startButton)
|
||
{
|
||
startButton.Click += OnStartButtonClick;
|
||
}
|
||
|
||
// 步骤 2: 主题选择页面
|
||
if (this.FindControl<Button>("ThemeBackButton") is { } themeBackButton)
|
||
{
|
||
themeBackButton.Click += OnThemeBackClick;
|
||
}
|
||
|
||
if (this.FindControl<Button>("ThemeNextButton") is { } themeNextButton)
|
||
{
|
||
themeNextButton.Click += OnThemeNextClick;
|
||
}
|
||
|
||
// 浅色/深色模式选择
|
||
if (this.FindControl<Border>("LightModeOption") is { } lightModeOption)
|
||
{
|
||
lightModeOption.PointerPressed += (s, e) => SelectThemeMode(Services.ThemeMode.Light);
|
||
}
|
||
|
||
if (this.FindControl<Border>("DarkModeOption") is { } darkModeOption)
|
||
{
|
||
darkModeOption.PointerPressed += (s, e) => SelectThemeMode(Services.ThemeMode.Dark);
|
||
}
|
||
|
||
if (this.FindControl<RadioButton>("LightModeRadio") is { } lightModeRadio)
|
||
{
|
||
lightModeRadio.IsCheckedChanged += (s, e) =>
|
||
{
|
||
if (lightModeRadio.IsChecked == true) SelectThemeMode(Services.ThemeMode.Light);
|
||
};
|
||
}
|
||
|
||
if (this.FindControl<RadioButton>("DarkModeRadio") is { } darkModeRadio)
|
||
{
|
||
darkModeRadio.IsCheckedChanged += (s, e) =>
|
||
{
|
||
if (darkModeRadio.IsChecked == true) SelectThemeMode(Services.ThemeMode.Dark);
|
||
};
|
||
}
|
||
|
||
// 主题色选择
|
||
SetupAccentColorHandlers();
|
||
|
||
// 莫奈取色来源选择
|
||
if (this.FindControl<Border>("MonetFromWallpaperOption") is { } monetWallpaperOption)
|
||
{
|
||
monetWallpaperOption.PointerPressed += (s, e) => SelectMonetSource(MonetSource.Wallpaper);
|
||
}
|
||
|
||
if (this.FindControl<Border>("MonetFromCustomOption") is { } monetCustomOption)
|
||
{
|
||
monetCustomOption.PointerPressed += (s, e) => SelectMonetSource(MonetSource.Custom);
|
||
}
|
||
|
||
if (this.FindControl<Border>("MonetDisabledOption") is { } monetDisabledOption)
|
||
{
|
||
monetDisabledOption.PointerPressed += (s, e) => SelectMonetSource(MonetSource.Disabled);
|
||
}
|
||
|
||
if (this.FindControl<RadioButton>("MonetFromWallpaperRadio") is { } monetWallpaperRadio)
|
||
{
|
||
monetWallpaperRadio.IsCheckedChanged += (s, e) =>
|
||
{
|
||
if (monetWallpaperRadio.IsChecked == true) SelectMonetSource(MonetSource.Wallpaper);
|
||
};
|
||
}
|
||
|
||
if (this.FindControl<RadioButton>("MonetFromCustomRadio") is { } monetCustomRadio)
|
||
{
|
||
monetCustomRadio.IsCheckedChanged += (s, e) =>
|
||
{
|
||
if (monetCustomRadio.IsChecked == true) SelectMonetSource(MonetSource.Custom);
|
||
};
|
||
}
|
||
|
||
if (this.FindControl<RadioButton>("MonetDisabledRadio") is { } monetDisabledRadio)
|
||
{
|
||
monetDisabledRadio.IsCheckedChanged += (s, e) =>
|
||
{
|
||
if (monetDisabledRadio.IsChecked == true) SelectMonetSource(MonetSource.Disabled);
|
||
};
|
||
}
|
||
|
||
// 步骤 3: 数据位置选择页面
|
||
if (this.FindControl<Button>("DataLocationBackButton") is { } dataLocationBackButton)
|
||
{
|
||
dataLocationBackButton.Click += OnDataLocationBackClick;
|
||
}
|
||
|
||
if (this.FindControl<Button>("DataLocationNextButton") is { } dataLocationNextButton)
|
||
{
|
||
dataLocationNextButton.Click += OnDataLocationNextClick;
|
||
}
|
||
|
||
if (this.FindControl<Border>("SystemOptionBorder") is { } systemOption)
|
||
{
|
||
systemOption.PointerPressed += (s, e) => SelectDataLocationMode(DataLocationMode.System);
|
||
}
|
||
|
||
if (this.FindControl<Border>("PortableOptionBorder") is { } portableOption)
|
||
{
|
||
portableOption.PointerPressed += (s, e) => SelectDataLocationMode(DataLocationMode.Portable);
|
||
}
|
||
|
||
if (this.FindControl<RadioButton>("SystemRadio") is { } systemRadio)
|
||
{
|
||
systemRadio.IsCheckedChanged += (s, e) =>
|
||
{
|
||
if (systemRadio.IsChecked == true) SelectDataLocationMode(DataLocationMode.System);
|
||
};
|
||
}
|
||
|
||
if (this.FindControl<RadioButton>("PortableRadio") is { } portableRadio)
|
||
{
|
||
portableRadio.IsCheckedChanged += (s, e) =>
|
||
{
|
||
if (portableRadio.IsChecked == true) SelectDataLocationMode(DataLocationMode.Portable);
|
||
};
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
if (this.FindControl<Button>("PrivacyNextButton") is { } privacyNextButton)
|
||
{
|
||
privacyNextButton.Click += OnPrivacyNextClick;
|
||
}
|
||
|
||
if (this.FindControl<Button>("ViewPrivacyPolicyButton") is { } viewPrivacyPolicyButton)
|
||
{
|
||
viewPrivacyPolicyButton.Click += OnViewPrivacyPolicyClick;
|
||
}
|
||
|
||
// 隐私协议复选框 - 控制遥测开关
|
||
if (this.FindControl<CheckBox>("PrivacyAgreementCheckBox") is { } privacyCheckBox)
|
||
{
|
||
privacyCheckBox.IsCheckedChanged += OnPrivacyAgreementChanged;
|
||
}
|
||
|
||
// 步骤 6: 欢迎完成页面
|
||
if (this.FindControl<Button>("EnterButton") is { } enterButton)
|
||
{
|
||
enterButton.Click += OnEnterClick;
|
||
}
|
||
}
|
||
|
||
private void SetupAccentColorHandlers()
|
||
{
|
||
var colorMap = new Dictionary<string, string>
|
||
{
|
||
{ "BlueColor", "#0078D4" },
|
||
{ "PurpleColor", "#7B68EE" },
|
||
{ "GreenColor", "#107C10" },
|
||
{ "OrangeColor", "#D83B01" },
|
||
{ "PinkColor", "#E3008C" },
|
||
{ "TealColor", "#008080" }
|
||
};
|
||
|
||
foreach (var (name, color) in colorMap)
|
||
{
|
||
if (this.FindControl<Border>(name) is { } colorBorder)
|
||
{
|
||
colorBorder.PointerPressed += (s, e) => SelectAccentColor(name, color);
|
||
}
|
||
}
|
||
}
|
||
|
||
private async void OnWindowOpened(object? sender, EventArgs e)
|
||
{
|
||
await PlayTypingAnimationAsync();
|
||
}
|
||
|
||
private async Task PlayTypingAnimationAsync()
|
||
{
|
||
var typingTextBlock = this.FindControl<TextBlock>("TypingTextBlock");
|
||
var cursorBorder = this.FindControl<Border>("CursorBorder");
|
||
var subtitlePanel = this.FindControl<StackPanel>("SubtitlePanel");
|
||
var buttonAnimationArea = this.FindControl<Grid>("ButtonAnimationArea");
|
||
var startButton = this.FindControl<Button>("StartButton");
|
||
var mouseCursor = this.FindControl<Canvas>("MouseCursor");
|
||
|
||
if (typingTextBlock == null || cursorBorder == null) return;
|
||
|
||
// 打字机效果:阑山桌面 LanMountain Desktop(在同一行)
|
||
var fullText = Strings.Oobe_TypingAppName;
|
||
for (int i = 0; i <= fullText.Length; i++)
|
||
{
|
||
typingTextBlock.Text = fullText.Substring(0, i);
|
||
await Task.Delay(TypingDelayMs);
|
||
}
|
||
|
||
// 停顿一下
|
||
await Task.Delay(500);
|
||
|
||
// 隐藏光标
|
||
cursorBorder.IsVisible = false;
|
||
|
||
// 显示副标题(打字机效果:下一代 互动信息看板)
|
||
if (subtitlePanel != null)
|
||
{
|
||
subtitlePanel.IsVisible = true;
|
||
subtitlePanel.Opacity = 1;
|
||
await PlaySubtitleTypingAnimationAsync();
|
||
}
|
||
|
||
// 停顿一下再显示按钮
|
||
await Task.Delay(400);
|
||
|
||
// 显示按钮动画区域
|
||
if (buttonAnimationArea != null)
|
||
{
|
||
buttonAnimationArea.IsVisible = true;
|
||
}
|
||
|
||
// 鼠标拖拽按钮入场
|
||
if (mouseCursor != null && startButton != null)
|
||
{
|
||
await AnimateMouseDragButtonAsync(mouseCursor, startButton);
|
||
}
|
||
}
|
||
|
||
private async Task AnimateMouseDragButtonAsync(Canvas mouseCursor, Button button)
|
||
{
|
||
// 初始处于画面外部的 X 坐标
|
||
var startX = -400.0;
|
||
var endX = 0.0;
|
||
|
||
button.IsVisible = true;
|
||
button.Opacity = 1;
|
||
button.RenderTransform = new TranslateTransform(startX, 0);
|
||
|
||
// 鼠标位于按钮上,比如偏移 (100, 30) 的位置
|
||
var mouseOffsetX = 100.0;
|
||
var mouseOffsetY = 30.0;
|
||
mouseCursor.Margin = new Thickness(startX + mouseOffsetX, mouseOffsetY, 0, 0);
|
||
mouseCursor.IsVisible = true;
|
||
|
||
await Task.Delay(300);
|
||
|
||
var duration = 800;
|
||
var steps = 40;
|
||
var delay = duration / steps;
|
||
|
||
for (int i = 0; i <= steps; i++)
|
||
{
|
||
var progress = (double)i / steps;
|
||
var eased = EaseOutBack(progress); // 使用 EaseOutBack 营造“拖拽到位”的清脆回弹感
|
||
|
||
var currentX = startX + (endX - startX) * eased;
|
||
|
||
button.RenderTransform = new TranslateTransform(currentX, 0);
|
||
mouseCursor.Margin = new Thickness(currentX + mouseOffsetX, mouseOffsetY, 0, 0);
|
||
|
||
await Task.Delay(delay);
|
||
}
|
||
|
||
await Task.Delay(200);
|
||
|
||
// 隐藏鼠标光标
|
||
await AnimateOpacityAsync(mouseCursor, 1, 0, 200);
|
||
mouseCursor.IsVisible = false;
|
||
}
|
||
|
||
private async Task PlaySubtitleTypingAnimationAsync()
|
||
{
|
||
var nextGenTextBlock = this.FindControl<TextBlock>("NextGenTextBlock");
|
||
var dashboardTextBlock = this.FindControl<TextBlock>("DashboardTextBlock");
|
||
var subtitleCursorBorder = this.FindControl<Border>("SubtitleCursorBorder");
|
||
|
||
if (nextGenTextBlock == null || dashboardTextBlock == null) return;
|
||
|
||
// 获取渐变画刷
|
||
var gradientBrush = nextGenTextBlock.Foreground as LinearGradientBrush;
|
||
|
||
// 启动渐变色流动动画
|
||
if (gradientBrush != null)
|
||
{
|
||
_ = AnimateGradientFlowAsync(gradientBrush);
|
||
}
|
||
|
||
// 显示光标
|
||
if (subtitleCursorBorder != null)
|
||
{
|
||
subtitleCursorBorder.IsVisible = true;
|
||
}
|
||
|
||
// 打字机效果:下一代
|
||
var nextGenText = Strings.Oobe_TypingNextGen;
|
||
for (int i = 0; i <= nextGenText.Length; i++)
|
||
{
|
||
nextGenTextBlock.Text = nextGenText.Substring(0, i);
|
||
await Task.Delay(TypingDelayMs);
|
||
}
|
||
|
||
// 停顿一下
|
||
await Task.Delay(200);
|
||
|
||
// 换行,光标移到第二行
|
||
if (subtitleCursorBorder != null)
|
||
{
|
||
subtitleCursorBorder.IsVisible = false;
|
||
}
|
||
|
||
// 打字机效果:互动信息看板
|
||
var dashboardText = Strings.Oobe_TypingDashboard;
|
||
for (int i = 0; i <= dashboardText.Length; i++)
|
||
{
|
||
dashboardTextBlock.Text = dashboardText.Substring(0, i);
|
||
await Task.Delay(TypingDelayMs);
|
||
}
|
||
|
||
// 停顿一下后隐藏光标
|
||
await Task.Delay(300);
|
||
}
|
||
|
||
private async Task AnimateGradientFlowAsync(LinearGradientBrush? gradientBrush)
|
||
{
|
||
if (gradientBrush == null) return;
|
||
|
||
var stops = gradientBrush.GradientStops;
|
||
if (stops.Count < 2) return;
|
||
|
||
// 获取原有的所有颜色
|
||
var colors = new System.Collections.Generic.List<Color>();
|
||
foreach (var stop in stops)
|
||
{
|
||
colors.Add(stop.Color);
|
||
}
|
||
|
||
// 为了实现无缝循环流动,把第一个颜色追加到最后
|
||
colors.Add(colors[0]);
|
||
|
||
// 重新分配 GradientStops
|
||
stops.Clear();
|
||
for (int i = 0; i < colors.Count; i++)
|
||
{
|
||
stops.Add(new GradientStop(colors[i], (double)i / (colors.Count - 1)));
|
||
}
|
||
|
||
// 设置铺展模式,超出范围时重复
|
||
gradientBrush.SpreadMethod = GradientSpreadMethod.Repeat;
|
||
|
||
double offset = 0;
|
||
|
||
while (true)
|
||
{
|
||
offset -= 0.005; // 每次流动一小步,负数表示向右流动
|
||
if (offset <= -1.0) offset = 0;
|
||
|
||
// 让渐变保持水平方向,但位置不断偏移,形成河流般的流动效果
|
||
gradientBrush.StartPoint = new RelativePoint(offset, 0, RelativeUnit.Relative);
|
||
gradientBrush.EndPoint = new RelativePoint(offset + 1, 0, RelativeUnit.Relative);
|
||
|
||
await Task.Delay(16); // 约60帧
|
||
}
|
||
}
|
||
|
||
private async void OnStartButtonClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (_isTransitioning) return;
|
||
await NavigateToStep(2);
|
||
}
|
||
|
||
// 主题选择页面按钮
|
||
private async void OnThemeBackClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (_isTransitioning) return;
|
||
await NavigateToStep(1);
|
||
}
|
||
|
||
private async void OnThemeNextClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (_isTransitioning) return;
|
||
await NavigateToStep(3);
|
||
}
|
||
|
||
// 数据位置选择页面按钮
|
||
private async void OnDataLocationBackClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (_isTransitioning) return;
|
||
await NavigateToStep(2);
|
||
}
|
||
|
||
private async void OnDataLocationNextClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (_isTransitioning) return;
|
||
|
||
// 应用数据位置选择
|
||
if (!_isDebugMode)
|
||
{
|
||
_resolver.ApplyLocationChoice(_selectedDataLocationMode, null, _migrateExistingData);
|
||
}
|
||
|
||
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()
|
||
? Strings.Oobe_AutoStartDesc
|
||
: Strings.Oobe_AutoStartDescNonWindows;
|
||
}
|
||
|
||
_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(4);
|
||
}
|
||
|
||
private async void OnPrivacyNextClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (_isTransitioning) return;
|
||
|
||
// 保存隐私设置
|
||
SavePrivacySettings();
|
||
|
||
await NavigateToStep(6);
|
||
}
|
||
|
||
private void OnViewPrivacyPolicyClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
// 打开隐私政策窗口
|
||
var privacyWindow = new PrivacyPolicyWindow
|
||
{
|
||
WindowStartupLocation = WindowStartupLocation.CenterOwner
|
||
};
|
||
privacyWindow.ShowDialog(this);
|
||
}
|
||
|
||
private void OnPrivacyAgreementChanged(object? sender, RoutedEventArgs e)
|
||
{
|
||
// 根据复选框状态控制遥测开关
|
||
if (this.FindControl<CheckBox>("PrivacyAgreementCheckBox") is { } checkBox &&
|
||
this.FindControl<ToggleSwitch>("CrashTelemetryToggle") is { } crashToggle &&
|
||
this.FindControl<ToggleSwitch>("UsageTelemetryToggle") is { } usageToggle)
|
||
{
|
||
var isAgreed = checkBox.IsChecked == true;
|
||
|
||
// 如果用户不同意协议,禁用遥测开关并关闭它们
|
||
crashToggle.IsEnabled = isAgreed;
|
||
usageToggle.IsEnabled = isAgreed;
|
||
|
||
if (!isAgreed)
|
||
{
|
||
crashToggle.IsChecked = false;
|
||
usageToggle.IsChecked = false;
|
||
}
|
||
else
|
||
{
|
||
// 用户同意协议后,默认开启遥测(用户可以在开关中手动关闭)
|
||
crashToggle.IsChecked = true;
|
||
usageToggle.IsChecked = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
private async void OnEnterClick(object? sender, RoutedEventArgs e)
|
||
{
|
||
if (_isTransitioning) return;
|
||
_isTransitioning = true;
|
||
|
||
try
|
||
{
|
||
await PlayExitAnimationAsync();
|
||
_completionSource.TrySetResult(true);
|
||
Close();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.Error.WriteLine($"[OobeWindow] Error: {ex.Message}");
|
||
_completionSource.TrySetResult(true);
|
||
Close();
|
||
}
|
||
}
|
||
|
||
private void InitializeDataLocationStep()
|
||
{
|
||
if (this.FindControl<TextBlock>("SystemPathText") is { } systemPathText)
|
||
{
|
||
systemPathText.Text = _resolver.DefaultSystemDataPath;
|
||
}
|
||
|
||
if (this.FindControl<TextBlock>("PortablePathText") is { } portablePathText)
|
||
{
|
||
portablePathText.Text = _resolver.DefaultPortableDataPath;
|
||
}
|
||
|
||
var canWriteToAppRoot = _resolver.IsPortableModeAllowed();
|
||
if (this.FindControl<RadioButton>("PortableRadio") is { } portableRadio)
|
||
{
|
||
portableRadio.IsEnabled = canWriteToAppRoot;
|
||
}
|
||
|
||
if (!canWriteToAppRoot)
|
||
{
|
||
if (this.FindControl<Border>("AdminWarningBanner") is { } warningBanner)
|
||
{
|
||
warningBanner.IsVisible = true;
|
||
}
|
||
}
|
||
|
||
if (_resolver.HasExistingSystemData())
|
||
{
|
||
_migrateExistingData = true;
|
||
if (this.FindControl<Border>("MigrationInfoBorder") is { } migrationInfo)
|
||
{
|
||
migrationInfo.IsVisible = true;
|
||
}
|
||
if (this.FindControl<TextBlock>("MigrationInfoText") is { } migrationText)
|
||
{
|
||
migrationText.Text = Strings.Oobe_MigrationDetected;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void SelectDataLocationMode(DataLocationMode mode)
|
||
{
|
||
_selectedDataLocationMode = mode;
|
||
|
||
if (this.FindControl<RadioButton>("SystemRadio") is { } systemRadio)
|
||
{
|
||
systemRadio.IsChecked = mode == DataLocationMode.System;
|
||
}
|
||
|
||
if (this.FindControl<RadioButton>("PortableRadio") is { } portableRadio)
|
||
{
|
||
portableRadio.IsChecked = mode == DataLocationMode.Portable;
|
||
}
|
||
|
||
if (this.FindControl<Border>("SystemOptionBorder") is { } systemBorder)
|
||
{
|
||
systemBorder.BorderBrush = mode == DataLocationMode.System
|
||
? Application.Current?.Resources["AccentFillColorDefaultBrush"] as IBrush
|
||
: Application.Current?.Resources["CardStrokeColorDefaultBrush"] as IBrush;
|
||
systemBorder.BorderThickness = mode == DataLocationMode.System
|
||
? new Thickness(2)
|
||
: new Thickness(1);
|
||
}
|
||
|
||
if (this.FindControl<Border>("PortableOptionBorder") is { } portableBorder)
|
||
{
|
||
portableBorder.BorderBrush = mode == DataLocationMode.Portable
|
||
? Application.Current?.Resources["AccentFillColorDefaultBrush"] as IBrush
|
||
: Application.Current?.Resources["CardStrokeColorDefaultBrush"] as IBrush;
|
||
portableBorder.BorderThickness = mode == DataLocationMode.Portable
|
||
? new Thickness(2)
|
||
: new Thickness(1);
|
||
}
|
||
}
|
||
|
||
// 主题选择方法
|
||
private void SelectThemeMode(Services.ThemeMode mode)
|
||
{
|
||
_selectedThemeMode = mode;
|
||
|
||
// 立即应用主题到启动器
|
||
ThemeService.ApplyTheme(mode, _selectedAccentColor);
|
||
|
||
if (this.FindControl<RadioButton>("LightModeRadio") is { } lightModeRadio)
|
||
{
|
||
lightModeRadio.IsChecked = mode == Services.ThemeMode.Light;
|
||
}
|
||
|
||
if (this.FindControl<RadioButton>("DarkModeRadio") is { } darkModeRadio)
|
||
{
|
||
darkModeRadio.IsChecked = mode == Services.ThemeMode.Dark;
|
||
}
|
||
|
||
if (this.FindControl<Border>("LightModeOption") is { } lightModeOption)
|
||
{
|
||
lightModeOption.BorderBrush = mode == Services.ThemeMode.Light
|
||
? Application.Current?.Resources["AccentFillColorDefaultBrush"] as IBrush
|
||
: Application.Current?.Resources["CardStrokeColorDefaultBrush"] as IBrush;
|
||
lightModeOption.BorderThickness = mode == Services.ThemeMode.Light
|
||
? new Thickness(2)
|
||
: new Thickness(1);
|
||
}
|
||
|
||
if (this.FindControl<Border>("DarkModeOption") is { } darkModeOption)
|
||
{
|
||
darkModeOption.BorderBrush = mode == Services.ThemeMode.Dark
|
||
? Application.Current?.Resources["AccentFillColorDefaultBrush"] as IBrush
|
||
: Application.Current?.Resources["CardStrokeColorDefaultBrush"] as IBrush;
|
||
darkModeOption.BorderThickness = mode == Services.ThemeMode.Dark
|
||
? new Thickness(2)
|
||
: new Thickness(1);
|
||
}
|
||
}
|
||
|
||
private void SelectAccentColor(string colorName, string colorValue)
|
||
{
|
||
_selectedAccentColor = colorValue;
|
||
|
||
// 更新所有颜色圆圈边框
|
||
var colorBorders = new[] { "BlueColor", "PurpleColor", "GreenColor", "OrangeColor", "PinkColor", "TealColor" };
|
||
foreach (var name in colorBorders)
|
||
{
|
||
if (this.FindControl<Border>(name) is { } border)
|
||
{
|
||
var isSelected = name == colorName;
|
||
border.BorderBrush = isSelected
|
||
? Application.Current?.Resources["TextFillColorPrimaryBrush"] as IBrush
|
||
: null;
|
||
border.BorderThickness = isSelected ? new Thickness(3) : new Thickness(0);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void SelectMonetSource(MonetSource source)
|
||
{
|
||
_selectedMonetSource = source;
|
||
|
||
if (this.FindControl<RadioButton>("MonetFromWallpaperRadio") is { } wallpaperRadio)
|
||
{
|
||
wallpaperRadio.IsChecked = source == MonetSource.Wallpaper;
|
||
}
|
||
|
||
if (this.FindControl<RadioButton>("MonetFromCustomRadio") is { } customRadio)
|
||
{
|
||
customRadio.IsChecked = source == MonetSource.Custom;
|
||
}
|
||
|
||
if (this.FindControl<RadioButton>("MonetDisabledRadio") is { } disabledRadio)
|
||
{
|
||
disabledRadio.IsChecked = source == MonetSource.Disabled;
|
||
}
|
||
|
||
UpdateMonetOptionBorder("MonetFromWallpaperOption", source == MonetSource.Wallpaper);
|
||
UpdateMonetOptionBorder("MonetFromCustomOption", source == MonetSource.Custom);
|
||
UpdateMonetOptionBorder("MonetDisabledOption", source == MonetSource.Disabled);
|
||
}
|
||
|
||
private void UpdateMonetOptionBorder(string borderName, bool isSelected)
|
||
{
|
||
if (this.FindControl<Border>(borderName) is { } border)
|
||
{
|
||
border.BorderBrush = isSelected
|
||
? Application.Current?.Resources["AccentFillColorDefaultBrush"] as IBrush
|
||
: Application.Current?.Resources["CardStrokeColorDefaultBrush"] as IBrush;
|
||
border.BorderThickness = isSelected ? new Thickness(2) : new Thickness(1);
|
||
}
|
||
}
|
||
|
||
private async Task NavigateToStep(int step)
|
||
{
|
||
if (_isTransitioning || step == _currentStep) return;
|
||
_isTransitioning = true;
|
||
|
||
// 获取当前步骤的控件
|
||
Grid? currentStepControl = _currentStep switch
|
||
{
|
||
1 => this.FindControl<Grid>("TypingStep"),
|
||
2 => this.FindControl<Grid>("ThemeStep"),
|
||
3 => this.FindControl<Grid>("DataLocationStep"),
|
||
4 => this.FindControl<Grid>("StartupPresentationStep"),
|
||
5 => this.FindControl<Grid>("PrivacyStep"),
|
||
6 => this.FindControl<Grid>("WelcomeStep"),
|
||
_ => null
|
||
};
|
||
|
||
// 获取目标步骤的控件
|
||
Grid? nextStepControl = step switch
|
||
{
|
||
1 => this.FindControl<Grid>("TypingStep"),
|
||
2 => this.FindControl<Grid>("ThemeStep"),
|
||
3 => this.FindControl<Grid>("DataLocationStep"),
|
||
4 => this.FindControl<Grid>("StartupPresentationStep"),
|
||
5 => this.FindControl<Grid>("PrivacyStep"),
|
||
6 => this.FindControl<Grid>("WelcomeStep"),
|
||
_ => null
|
||
};
|
||
|
||
if (currentStepControl == null || nextStepControl == null)
|
||
{
|
||
_isTransitioning = false;
|
||
return;
|
||
}
|
||
|
||
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);
|
||
|
||
_currentStep = step;
|
||
_isTransitioning = false;
|
||
}
|
||
|
||
private async Task PlayExitAnimationAsync()
|
||
{
|
||
var contentGrid = this.FindControl<Grid>("ContentGrid");
|
||
if (contentGrid != null)
|
||
{
|
||
await AnimateOpacityAsync(contentGrid, 1, 0, AnimationDurationMs);
|
||
}
|
||
}
|
||
|
||
private static async Task AnimateOpacityAsync(Control element, double from, double to, int durationMs)
|
||
{
|
||
var steps = 20;
|
||
var delay = durationMs / steps;
|
||
|
||
for (int i = 0; i <= steps; i++)
|
||
{
|
||
var progress = (double)i / steps;
|
||
var eased = EaseOutCubic(progress);
|
||
element.Opacity = from + (to - from) * eased;
|
||
await Task.Delay(delay);
|
||
}
|
||
}
|
||
|
||
private static double EaseOutCubic(double t) => 1 - Math.Pow(1 - t, 3);
|
||
private static double EaseOutQuad(double t) => 1 - Math.Pow(1 - t, 2);
|
||
private static double EaseOutBack(double t)
|
||
{
|
||
const double c1 = 1.70158;
|
||
const double c3 = c1 + 1;
|
||
var t1 = t - 1;
|
||
return 1 + c3 * Math.Pow(t1, 3) + c1 * Math.Pow(t1, 2);
|
||
}
|
||
|
||
private void InitializePrivacySettings()
|
||
{
|
||
// 生成隐私追踪 ID
|
||
var telemetryId = Guid.NewGuid().ToString("N");
|
||
if (this.FindControl<TextBox>("TelemetryIdTextBox") is { } telemetryIdTextBox)
|
||
{
|
||
telemetryIdTextBox.Text = telemetryId;
|
||
}
|
||
}
|
||
|
||
private void SavePrivacySettings()
|
||
{
|
||
try
|
||
{
|
||
var crashTelemetryEnabled = this.FindControl<ToggleSwitch>("CrashTelemetryToggle")?.IsChecked ?? true;
|
||
var usageTelemetryEnabled = this.FindControl<ToggleSwitch>("UsageTelemetryToggle")?.IsChecked ?? true;
|
||
var telemetryId = this.FindControl<TextBox>("TelemetryIdTextBox")?.Text ?? Guid.NewGuid().ToString("N");
|
||
|
||
// 保存到启动器配置
|
||
var privacyConfig = new PrivacyConfig
|
||
{
|
||
CrashTelemetryEnabled = crashTelemetryEnabled,
|
||
UsageTelemetryEnabled = usageTelemetryEnabled,
|
||
TelemetryId = telemetryId
|
||
};
|
||
|
||
var configPath = Path.Combine(_resolver.ResolveLauncherDataPath(), "privacy-config.json");
|
||
var json = System.Text.Json.JsonSerializer.Serialize(privacyConfig, AppJsonContext.Default.PrivacyConfig);
|
||
File.WriteAllText(configPath, json);
|
||
|
||
// 保存隐私协议同意状态(带防篡改保护)
|
||
var agreementService = new PrivacyAgreementService(_resolver.ResolveLauncherDataPath());
|
||
var isAgreed = this.FindControl<CheckBox>("PrivacyAgreementCheckBox")?.IsChecked ?? false;
|
||
|
||
// 生成用户ID和设备ID
|
||
var userId = telemetryId;
|
||
var deviceId = GetDeviceIdentifier();
|
||
|
||
agreementService.SaveAgreement(isAgreed, userId, deviceId);
|
||
|
||
Logger.Info($"[OobeWindow] 隐私设置已保存: Crash={crashTelemetryEnabled}, Usage={usageTelemetryEnabled}, Agreement={isAgreed}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Logger.Warn($"[OobeWindow] 保存隐私设置失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取设备标识符
|
||
/// </summary>
|
||
private string GetDeviceIdentifier()
|
||
{
|
||
try
|
||
{
|
||
// 使用机器名和用户名的组合作为设备标识
|
||
var machineName = Environment.MachineName;
|
||
var userName = Environment.UserName;
|
||
|
||
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
||
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes($"{machineName}:{userName}"));
|
||
return Convert.ToHexString(hash).Substring(0, 16);
|
||
}
|
||
catch
|
||
{
|
||
return "UnknownDevice";
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// 枚举定义(使用 Services 命名空间中的 ThemeMode)
|
||
public enum MonetSource
|
||
{
|
||
Wallpaper,
|
||
Custom,
|
||
Disabled
|
||
}
|