Add OOBE redesign, theme & data location support

Introduce a redesigned OOBE flow and data-location/theme support across the launcher. Adds a new ThemeService for applying light/dark and accent colors; integrates FluentIcons.Avalonia package for icons. Overhauls OobeWindow (UX animations, typing effect, multi-step theme and data-location pages, Monet options, and final welcome step) and its code-behind to handle step navigation, accent selection, and data-location resolution. Adds DataLocation UI and handlers (DataLocationPromptWindow changes, DataLocation resolver usage) and wires a DevDebug UI for toggling/opening the data-location page. UpdateEngineService now resolves the launcher root via DataLocationResolver. Misc: update various view models, localization entries and remove TrimmerRoots.xml.
This commit is contained in:
lincube
2026-04-25 17:29:25 +08:00
parent 8b8c7d1e7f
commit 5b4b9f32b5
21 changed files with 1580 additions and 208 deletions

View File

@@ -25,6 +25,7 @@
<PackageReference Include="Avalonia" Version="11.3.12" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
<PackageReference Include="FluentAvaloniaUI" Version="2.5.0" />
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.250403001" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
<PackageReference Include="Tmds.DBus.Protocol" Version="0.92.0" />
</ItemGroup>

View File

@@ -0,0 +1,68 @@
using Avalonia;
using Avalonia.Styling;
using FluentAvalonia.Styling;
namespace LanMountainDesktop.Launcher.Services;
/// <summary>
/// 主题服务,管理启动器的主题设置
/// </summary>
public static class ThemeService
{
private static ThemeVariant _currentTheme = ThemeVariant.Light;
private static string _accentColor = "#0078D4";
/// <summary>
/// 获取当前主题
/// </summary>
public static ThemeVariant CurrentTheme => _currentTheme;
/// <summary>
/// 获取当前主题色
/// </summary>
public static string AccentColor => _accentColor;
/// <summary>
/// 应用主题设置
/// </summary>
public static void ApplyTheme(ThemeMode mode, string accentColor)
{
_currentTheme = mode switch
{
ThemeMode.Dark => ThemeVariant.Dark,
_ => ThemeVariant.Light
};
_accentColor = accentColor;
// 应用到当前应用程序
if (Application.Current is { } app)
{
app.RequestedThemeVariant = _currentTheme;
}
}
/// <summary>
/// 应用浅色主题
/// </summary>
public static void ApplyLightTheme(string accentColor)
{
ApplyTheme(ThemeMode.Light, accentColor);
}
/// <summary>
/// 应用深色主题
/// </summary>
public static void ApplyDarkTheme(string accentColor)
{
ApplyTheme(ThemeMode.Dark, accentColor);
}
}
/// <summary>
/// 主题模式
/// </summary>
public enum ThemeMode
{
Light,
Dark
}

View File

@@ -22,6 +22,7 @@ internal sealed class UpdateEngineService
private readonly DeploymentLocator _deploymentLocator;
private readonly string _appRoot;
private readonly string _dataRoot;
private readonly string _launcherRoot;
private readonly string _incomingRoot;
private readonly string _snapshotsRoot;
@@ -30,7 +31,8 @@ internal sealed class UpdateEngineService
{
_deploymentLocator = deploymentLocator;
_appRoot = deploymentLocator.GetAppRoot();
_launcherRoot = Path.Combine(_appRoot, LauncherDirectoryName);
_dataRoot = new DataLocationResolver(_appRoot).ResolveDataRoot();
_launcherRoot = Path.Combine(_dataRoot, LauncherDirectoryName);
_incomingRoot = Path.Combine(_launcherRoot, UpdateDirectoryName, IncomingDirectoryName);
_snapshotsRoot = Path.Combine(_launcherRoot, SnapshotsDirectoryName);
}

View File

@@ -13,6 +13,7 @@ public sealed class DevDebugWindowViewModel : INotifyPropertyChanged
private bool _isErrorEnabled = true;
private bool _isUpdateEnabled = true;
private bool _isOobeEnabled = true;
private bool _isDataLocationEnabled = true;
private string _statusMessage = "就绪";
public event PropertyChangedEventHandler? PropertyChanged;
@@ -87,6 +88,23 @@ public sealed class DevDebugWindowViewModel : INotifyPropertyChanged
}
}
/// <summary>
/// 数据位置选择页面是否启用实际功能
/// </summary>
public bool IsDataLocationEnabled
{
get => _isDataLocationEnabled;
set
{
if (_isDataLocationEnabled != value)
{
_isDataLocationEnabled = value;
OnPropertyChanged();
UpdateStatus($"数据位置选择: {(value ? "" : "")}");
}
}
}
#endregion
#region
@@ -131,6 +149,11 @@ public sealed class DevDebugWindowViewModel : INotifyPropertyChanged
/// </summary>
public ICommand OpenOobeCommand { get; }
/// <summary>
/// 打开数据位置选择页面命令
/// </summary>
public ICommand OpenDataLocationCommand { get; }
/// <summary>
/// 全部切换到查看模式命令
/// </summary>
@@ -170,6 +193,11 @@ public sealed class DevDebugWindowViewModel : INotifyPropertyChanged
/// </summary>
public event EventHandler<OobeOpenEventArgs>? OpenOobeRequested;
/// <summary>
/// 请求打开数据位置选择页面
/// </summary>
public event EventHandler<DataLocationOpenEventArgs>? OpenDataLocationRequested;
/// <summary>
/// 请求关闭窗口
/// </summary>
@@ -199,12 +227,18 @@ public sealed class DevDebugWindowViewModel : INotifyPropertyChanged
OpenOobeRequested?.Invoke(this, new OobeOpenEventArgs(IsOobeEnabled));
});
OpenDataLocationCommand = new RelayCommand(() =>
{
OpenDataLocationRequested?.Invoke(this, new DataLocationOpenEventArgs(IsDataLocationEnabled));
});
SetAllViewOnlyCommand = new RelayCommand(() =>
{
IsSplashEnabled = false;
IsErrorEnabled = false;
IsUpdateEnabled = false;
IsOobeEnabled = false;
IsDataLocationEnabled = false;
UpdateStatus("全部页面已切换到查看模式");
});
@@ -214,6 +248,7 @@ public sealed class DevDebugWindowViewModel : INotifyPropertyChanged
IsErrorEnabled = true;
IsUpdateEnabled = true;
IsOobeEnabled = true;
IsDataLocationEnabled = true;
UpdateStatus("全部页面已切换到功能模式");
});
@@ -260,4 +295,10 @@ public class OobeOpenEventArgs : EventArgs
public OobeOpenEventArgs(bool isFunctional) => IsFunctional = isFunctional;
}
public class DataLocationOpenEventArgs : EventArgs
{
public bool IsFunctional { get; }
public DataLocationOpenEventArgs(bool isFunctional) => IsFunctional = isFunctional;
}
#endregion

View File

@@ -221,6 +221,7 @@ internal partial class DataLocationPromptWindow : Window
{
Duration = TimeSpan.FromMilliseconds(500),
Easing = new CubicEaseOut(),
FillMode = FillMode.Forward,
Children =
{
new KeyFrame
@@ -240,6 +241,7 @@ internal partial class DataLocationPromptWindow : Window
{
Duration = TimeSpan.FromMilliseconds(500),
Easing = new CubicEaseOut(),
FillMode = FillMode.Forward,
Children =
{
new KeyFrame
@@ -280,6 +282,7 @@ internal partial class DataLocationPromptWindow : Window
{
Duration = TimeSpan.FromMilliseconds(200),
Easing = new CubicEaseIn(),
FillMode = FillMode.Forward,
Children =
{
new KeyFrame

View File

@@ -141,6 +141,32 @@
</Grid>
</Border>
<!-- 数据位置选择页面 -->
<Border Background="{DynamicResource SystemControlBackgroundAltMediumBrush}"
CornerRadius="8"
Padding="15">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock Text="📁 数据位置选择 (DataLocationPromptWindow)"
FontWeight="SemiBold"
FontSize="14" />
<TextBlock Text="选择数据保存位置"
FontSize="11"
Opacity="0.6"
Margin="0,3,0,0" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="8">
<ToggleSwitch Content="启用功能"
IsChecked="{Binding IsDataLocationEnabled}"
OnContent="功能"
OffContent="查看" />
<Button Content="打开"
Command="{Binding OpenDataLocationCommand}"
HorizontalAlignment="Right" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
</ScrollViewer>

View File

@@ -25,6 +25,7 @@ public partial class DevDebugWindow : Window
_viewModel.OpenErrorRequested += OnOpenErrorRequested;
_viewModel.OpenUpdateRequested += OnOpenUpdateRequested;
_viewModel.OpenOobeRequested += OnOpenOobeRequested;
_viewModel.OpenDataLocationRequested += OnOpenDataLocationRequested;
_viewModel.CloseRequested += OnCloseRequested;
}
@@ -135,6 +136,17 @@ public partial class DevDebugWindow : Window
}
}
/// <summary>
/// 打开数据位置选择页面
/// </summary>
private void OnOpenDataLocationRequested(object? sender, DataLocationOpenEventArgs e)
{
var appRoot = AppDomain.CurrentDomain.BaseDirectory;
var resolver = new DataLocationResolver(appRoot);
var window = new DataLocationPromptWindow(resolver);
window.Show();
}
/// <summary>
/// 关闭窗口
/// </summary>

View File

@@ -4,13 +4,14 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia"
mc:Ignorable="d"
d:DesignWidth="600"
d:DesignWidth="700"
d:DesignHeight="500"
x:Class="LanMountainDesktop.Launcher.Views.OobeWindow"
x:DataType="views:OobeWindow"
Title="欢迎使用阑山桌面"
Width="600"
Width="700"
Height="500"
CanResize="False"
WindowStartupLocation="CenterScreen"
@@ -21,59 +22,600 @@
<views:OobeWindow />
</Design.DataContext>
<Grid x:Name="ContentGrid"
Opacity="0">
<Grid.RenderTransform>
<TranslateTransform Y="24" />
</Grid.RenderTransform>
<!-- 主内容区域 -->
<Grid Margin="48" RowDefinitions="*,Auto">
<!-- 中央内容区域 -->
<StackPanel Grid.Row="0"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Spacing="24">
<!-- 顶部:完成状态勾号图标 -->
<Border Width="80"
Height="80"
Background="{DynamicResource SystemFillColorSuccessBackgroundBrush}"
CornerRadius="40"
HorizontalAlignment="Center">
<ui:SymbolIcon Symbol="Accept"
FontSize="40"
Foreground="{DynamicResource SystemFillColorSuccessBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<!-- 中央:欢迎文字 -->
<StackPanel Spacing="8" HorizontalAlignment="Center">
<TextBlock Text="欢迎使用阑山桌面"
<Grid x:Name="ContentGrid">
<!-- 步骤 1: 打字机动画开场 -->
<Grid x:Name="TypingStep" Margin="60,80,60,60">
<!-- 主标题区域(左上角) -->
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Spacing="16">
<!-- 打字机文本区域 -->
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="TypingTextBlock"
FontSize="28"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
FontFamily="Consolas, Monaco, 'Courier New', monospace" />
<Border x:Name="CursorBorder"
Width="3"
Height="28"
Background="{DynamicResource TextFillColorPrimaryBrush}"
VerticalAlignment="Bottom"
Margin="4,0,0,4">
<Border.Styles>
<Style Selector="Border">
<Style.Animations>
<Animation Duration="0:0:0.8" IterationCount="INFINITE">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="1" />
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="Opacity" Value="1" />
</KeyFrame>
<KeyFrame Cue="51%">
<Setter Property="Opacity" Value="0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Border.Styles>
</Border>
</StackPanel>
<!-- 副标题区域(流光渐变动画 + 打字机效果) -->
<StackPanel x:Name="SubtitlePanel" Opacity="0" IsVisible="False" Spacing="4">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="NextGenTextBlock"
FontSize="48"
FontWeight="Bold"
FontFamily="Consolas, Monaco, 'Courier New', monospace">
<TextBlock.Foreground>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Offset="0.0" Color="#0078D4" />
<GradientStop Offset="0.33" Color="#7B68EE" />
<GradientStop Offset="0.66" Color="#FF8C00" />
<GradientStop Offset="1.0" Color="#107C10" />
</LinearGradientBrush>
</TextBlock.Foreground>
</TextBlock> <Border x:Name="SubtitleCursorBorder"
Width="4"
Height="48"
Background="{DynamicResource TextFillColorPrimaryBrush}"
VerticalAlignment="Bottom"
Margin="4,0,0,4"
IsVisible="False">
<Border.Styles>
<Style Selector="Border">
<Style.Animations>
<Animation Duration="0:0:0.8" IterationCount="INFINITE">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="1" />
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="Opacity" Value="1" />
</KeyFrame>
<KeyFrame Cue="51%">
<Setter Property="Opacity" Value="0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Border.Styles>
</Border>
</StackPanel>
<TextBlock x:Name="DashboardTextBlock"
FontSize="48"
FontWeight="Bold"
FontFamily="Consolas, Monaco, 'Courier New', monospace"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</StackPanel>
</StackPanel>
<!-- 按钮动画区域(左下角) -->
<Grid x:Name="ButtonAnimationArea"
Width="280"
Height="80"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Margin="0,0,0,40"
IsVisible="False">
<!-- 方框边框(由鼠标画出) -->
<Border x:Name="DrawnBorder"
Width="160"
Height="56"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="Transparent"
BorderBrush="{DynamicResource AccentFillColorDefaultBrush}"
BorderThickness="0"
CornerRadius="{DynamicResource DesignCornerRadiusMd}">
</Border>
<!-- 开始按钮(从方框中弹出) -->
<Button x:Name="StartButton"
Width="160"
Height="56"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Theme="{DynamicResource AccentButtonTheme}"
Opacity="0"
IsVisible="False"
RenderTransformOrigin="0.5,0.5">
<Button.RenderTransform>
<ScaleTransform ScaleX="0.1" ScaleY="0.1" />
</Button.RenderTransform>
<TextBlock Text="开始使用"
FontSize="16"
FontWeight="SemiBold" />
</Button>
<!-- 鼠标光标 -->
<Canvas x:Name="MouseCursor"
Width="24"
Height="24"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="-50,-50,0,0"
IsVisible="False">
<Path Data="M0,0 L0,18 L4,14 L7,20 L10,19 L7,13 L12,13 Z"
Fill="{DynamicResource TextFillColorPrimaryBrush}"
Stroke="{DynamicResource SolidBackgroundFillColorBaseBrush}"
StrokeThickness="1" />
</Canvas>
</Grid>
</Grid>
<!-- 步骤 2: 主题选择页面 -->
<Grid x:Name="ThemeStep" Margin="48" RowDefinitions="Auto,*,Auto" IsVisible="False">
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,24">
<TextBlock Text="个性化你的桌面"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="选择你喜欢的主题样式,可随时在设置中更改"
FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="20">
<!-- 浅色/深色模式选择 -->
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="16">
<StackPanel Spacing="12">
<TextBlock Text="外观模式"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<Grid ColumnDefinitions="*,*" ColumnSpacing="12">
<Border x:Name="LightModeOption"
Grid.Column="0"
Background="{DynamicResource SolidBackgroundFillColorQuarternaryBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
BorderThickness="2"
BorderBrush="{DynamicResource AccentFillColorDefaultBrush}"
Padding="16"
Cursor="Hand">
<StackPanel Spacing="8" HorizontalAlignment="Center">
<Border Width="48"
Height="48"
Background="#F3F3F3"
CornerRadius="8"
BorderBrush="#E0E0E0"
BorderThickness="1">
<fi:SymbolIcon Symbol="WeatherSunny"
FontSize="24"
Foreground="#5F5F5F"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<TextBlock Text="浅色模式"
FontSize="13"
HorizontalAlignment="Center"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<RadioButton x:Name="LightModeRadio"
GroupName="ThemeMode"
IsChecked="True"
HorizontalAlignment="Center" />
</StackPanel>
</Border>
<Border x:Name="DarkModeOption"
Grid.Column="1"
Background="{DynamicResource SolidBackgroundFillColorQuarternaryBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
BorderThickness="1"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
Padding="16"
Cursor="Hand">
<StackPanel Spacing="8" HorizontalAlignment="Center">
<Border Width="48"
Height="48"
Background="#1E1E1E"
CornerRadius="8"
BorderBrush="#333333"
BorderThickness="1">
<fi:SymbolIcon Symbol="WeatherMoon"
FontSize="24"
Foreground="#E0E0E0"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<TextBlock Text="深色模式"
FontSize="13"
HorizontalAlignment="Center"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<RadioButton x:Name="DarkModeRadio"
GroupName="ThemeMode"
HorizontalAlignment="Center" />
</StackPanel>
</Border>
</Grid>
</StackPanel>
</Border>
<!-- 主题色选择 -->
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="16">
<StackPanel Spacing="12">
<TextBlock Text="主题色"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<WrapPanel x:Name="AccentColorPanel" HorizontalAlignment="Left">
<!-- 预设颜色 -->
<Border x:Name="BlueColor"
Width="40"
Height="40"
Background="#0078D4"
CornerRadius="20"
BorderThickness="3"
BorderBrush="{DynamicResource AccentFillColorDefaultBrush}"
Margin="0,0,12,12"
Cursor="Hand">
<Border.Styles>
<Style Selector="Border:pointerover">
<Setter Property="RenderTransform" Value="scale(1.1)" />
</Style>
</Border.Styles>
</Border>
<Border x:Name="PurpleColor"
Width="40"
Height="40"
Background="#7B68EE"
CornerRadius="20"
BorderThickness="0"
Margin="0,0,12,12"
Cursor="Hand" />
<Border x:Name="GreenColor"
Width="40"
Height="40"
Background="#107C10"
CornerRadius="20"
BorderThickness="0"
Margin="0,0,12,12"
Cursor="Hand" />
<Border x:Name="OrangeColor"
Width="40"
Height="40"
Background="#D83B01"
CornerRadius="20"
BorderThickness="0"
Margin="0,0,12,12"
Cursor="Hand" />
<Border x:Name="PinkColor"
Width="40"
Height="40"
Background="#E3008C"
CornerRadius="20"
BorderThickness="0"
Margin="0,0,12,12"
Cursor="Hand" />
<Border x:Name="TealColor"
Width="40"
Height="40"
Background="#008080"
CornerRadius="20"
BorderThickness="0"
Margin="0,0,12,12"
Cursor="Hand" />
</WrapPanel>
</StackPanel>
</Border>
<!-- 莫奈取色来源 -->
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="16">
<StackPanel Spacing="12">
<TextBlock Text="莫奈取色来源"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="从壁纸自动提取主题色,让界面与桌面完美融合"
FontSize="12"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<StackPanel Spacing="8">
<Border x:Name="MonetFromWallpaperOption"
Background="{DynamicResource SolidBackgroundFillColorQuarternaryBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
BorderThickness="2"
BorderBrush="{DynamicResource AccentFillColorDefaultBrush}"
Padding="12"
Cursor="Hand">
<Grid ColumnDefinitions="Auto,*">
<RadioButton x:Name="MonetFromWallpaperRadio"
Grid.Column="0"
GroupName="MonetSource"
IsChecked="True"
VerticalAlignment="Center"
Margin="0,0,12,0" />
<StackPanel Grid.Column="1" Spacing="4">
<TextBlock Text="从桌面壁纸取色"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="自动分析当前壁纸颜色生成主题"
FontSize="11"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Border>
<Border x:Name="MonetFromCustomOption"
Background="{DynamicResource SolidBackgroundFillColorQuarternaryBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
BorderThickness="1"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
Padding="12"
Cursor="Hand">
<Grid ColumnDefinitions="Auto,*">
<RadioButton x:Name="MonetFromCustomRadio"
Grid.Column="0"
GroupName="MonetSource"
VerticalAlignment="Center"
Margin="0,0,12,0" />
<StackPanel Grid.Column="1" Spacing="4">
<TextBlock Text="自定义图片取色"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="选择一张图片作为取色来源"
FontSize="11"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Border>
<Border x:Name="MonetDisabledOption"
Background="{DynamicResource SolidBackgroundFillColorQuarternaryBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
BorderThickness="1"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
Padding="12"
Cursor="Hand">
<Grid ColumnDefinitions="Auto,*">
<RadioButton x:Name="MonetDisabledRadio"
Grid.Column="0"
GroupName="MonetSource"
VerticalAlignment="Center"
Margin="0,0,12,0" />
<StackPanel Grid.Column="1" Spacing="4">
<TextBlock Text="不使用莫奈取色"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="使用固定的预设主题色"
FontSize="11"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<StackPanel Grid.Row="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="12"
Margin="0,24,0,0">
<Button x:Name="ThemeBackButton"
Content="返回"
Theme="{DynamicResource ButtonTheme}" />
<Button x:Name="ThemeNextButton"
Content="下一步"
Theme="{DynamicResource AccentButtonTheme}" />
</StackPanel>
</Grid>
<!-- 步骤 3: 数据位置选择页面 -->
<Grid x:Name="DataLocationStep" Margin="48" RowDefinitions="Auto,*,Auto" IsVisible="False">
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,24">
<TextBlock Text="选择数据保存位置"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="决定将应用数据保存在哪里,可随时在设置中更改"
FontSize="13"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
<StackPanel Grid.Row="1" Spacing="16">
<Border x:Name="AdminWarningBanner"
Background="{DynamicResource SystemFillColorCriticalBackgroundBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="12,10"
IsVisible="False">
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="6">
<PathIcon Data="M12,2 L1,21 L23,21 Z M11,9 L13,9 L13,15 L11,15 Z M11,17 L13,17 L13,19 L11,19 Z"
Width="16"
Height="16"
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="无法保存到应用目录"
FontWeight="SemiBold"
FontSize="13"
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
</StackPanel>
<TextBlock Text="当前安装目录需要管理员权限才能写入,数据将自动保存到系统用户目录。"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
</StackPanel>
</Border>
<Border x:Name="SystemOptionBorder"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
BorderThickness="2"
BorderBrush="{DynamicResource AccentFillColorDefaultBrush}"
Padding="16,14"
Cursor="Hand">
<Grid ColumnDefinitions="Auto,*">
<RadioButton x:Name="SystemRadio"
Grid.Column="0"
VerticalAlignment="Top"
Margin="0,2,12,0"
GroupName="DataLocation"
IsChecked="True" />
<StackPanel Grid.Column="1" Spacing="4">
<TextBlock Text="保存在系统用户目录(推荐)"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="数据与当前 Windows 用户绑定,重装应用或更新后数据不会丢失"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="SystemPathText"
FontSize="11"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Margin="0,4,0,0" />
</StackPanel>
</Grid>
</Border>
<Border x:Name="PortableOptionBorder"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
BorderThickness="1"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
Padding="16,14"
Cursor="Hand">
<Grid ColumnDefinitions="Auto,*">
<RadioButton x:Name="PortableRadio"
Grid.Column="0"
VerticalAlignment="Top"
Margin="0,2,12,0"
GroupName="DataLocation"
IsEnabled="False" />
<StackPanel Grid.Column="1" Spacing="4">
<TextBlock Text="保存在应用安装目录"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="便于携带,可随应用文件夹整体移动到其他电脑"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="PortablePathText"
FontSize="11"
TextWrapping="Wrap"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
Margin="0,4,0,0" />
</StackPanel>
</Grid>
</Border>
<Border x:Name="MigrationInfoBorder"
Background="{DynamicResource SystemFillColorSuccessBackgroundBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="12,10"
IsVisible="False">
<StackPanel Orientation="Horizontal" Spacing="6">
<PathIcon Data="M9,16.17 L4.83,12 L3.41,13.41 L9,19 L21,7 L19.59,5.59 Z"
Width="16"
Height="16"
Foreground="{DynamicResource SystemFillColorSuccessBrush}" />
<TextBlock x:Name="MigrationInfoText"
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource SystemFillColorSuccessBrush}" />
</StackPanel>
</Border>
</StackPanel>
<StackPanel Grid.Row="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="12"
Margin="0,24,0,0">
<Button x:Name="DataLocationBackButton"
Content="返回"
Theme="{DynamicResource ButtonTheme}" />
<Button x:Name="DataLocationNextButton"
Content="下一步"
Theme="{DynamicResource AccentButtonTheme}" />
</StackPanel>
</Grid>
<!-- 步骤 4: 欢迎完成页面 -->
<Grid x:Name="WelcomeStep" Margin="48" RowDefinitions="*,Auto" IsVisible="False">
<StackPanel Grid.Row="0"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Spacing="32">
<Border Width="96"
Height="96"
Background="{DynamicResource SystemFillColorSuccessBackgroundBrush}"
CornerRadius="48"
HorizontalAlignment="Center">
<PathIcon Data="M9,16.17 L4.83,12 L3.41,13.41 L9,19 L21,7 L19.59,5.59 Z"
Width="48"
Height="48"
Foreground="{DynamicResource SystemFillColorSuccessBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<StackPanel Spacing="12" HorizontalAlignment="Center">
<TextBlock Text="欢迎使用阑山桌面"
FontSize="32"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
HorizontalAlignment="Center" />
<TextBlock Text="你的桌面,不止一面"
FontSize="14"
FontSize="16"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
HorizontalAlignment="Center" />
</StackPanel>
</StackPanel>
<!-- 底部:圆形开始按钮 -->
<Button Grid.Row="1"
x:Name="EnterButton"
HorizontalAlignment="Center"
Width="56"
Height="56"
Margin="0,0,0,16"
Margin="0,0,0,24"
Theme="{DynamicResource AccentButtonTheme}"
CornerRadius="28">
<ui:SymbolIcon Symbol="Forward"
<fi:SymbolIcon Symbol="ArrowRight"
FontSize="24"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"/>
Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
</Button>
</Grid>
</Grid>

View File

@@ -1,182 +1,709 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.Launcher.Models;
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;
public OobeWindow()
{
AvaloniaXamlLoader.Load(this);
Loaded += OnWindowLoaded;
Opened += OnWindowOpened;
var appRoot = AppDomain.CurrentDomain.BaseDirectory;
_resolver = new DataLocationResolver(appRoot);
}
public void SetDebugMode(bool isDebugMode)
{
_isDebugMode = isDebugMode;
}
public Task WaitForEnterAsync() => _completionSource.Task;
private void OnWindowLoaded(object? sender, RoutedEventArgs e)
{
Console.WriteLine("[OobeWindow] Window loaded, initializing components...");
InitializeDataLocationStep();
SetupEventHandlers();
}
var enterButton = this.FindControl<Button>("EnterButton");
if (enterButton is not null)
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);
};
}
// 步骤 4: 欢迎完成页面
if (this.FindControl<Button>("EnterButton") is { } enterButton)
{
enterButton.Click += OnEnterClick;
Console.WriteLine("[OobeWindow] EnterButton event bound successfully");
}
else
}
private void SetupAccentColorHandlers()
{
var colorMap = new Dictionary<string, string>
{
Console.Error.WriteLine("[OobeWindow] Failed to find EnterButton!");
{ "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)
{
Console.WriteLine("[OobeWindow] Window opened, playing entrance animation...");
await PlayEntranceAnimationAsync();
await PlayTypingAnimationAsync();
}
private async Task PlayEntranceAnimationAsync()
private async Task PlayTypingAnimationAsync()
{
try
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 = "阑山桌面 LanMountain Desktop";
for (int i = 0; i <= fullText.Length; i++)
{
var contentGrid = this.FindControl<Grid>("ContentGrid");
if (contentGrid is null)
{
return;
}
var translateTransform = contentGrid.RenderTransform as TranslateTransform ?? new TranslateTransform();
contentGrid.RenderTransform = translateTransform;
var offset = ResolveEntranceOffset();
contentGrid.Opacity = 0;
translateTransform.Y = offset;
var fadeInAnimation = new Animation
{
Duration = TimeSpan.FromMilliseconds(600),
Easing = new CubicEaseOut(),
Children =
{
new KeyFrame
{
Setters = { new Setter(OpacityProperty, 0.0) },
KeyTime = TimeSpan.FromMilliseconds(0)
},
new KeyFrame
{
Setters = { new Setter(OpacityProperty, 1.0) },
KeyTime = TimeSpan.FromMilliseconds(600)
}
}
};
var slideUpAnimation = new Animation
{
Duration = TimeSpan.FromMilliseconds(600),
Easing = new CubicEaseOut(),
Children =
{
new KeyFrame
{
Setters = { new Setter(TranslateTransform.YProperty, offset) },
KeyTime = TimeSpan.FromMilliseconds(0)
},
new KeyFrame
{
Setters = { new Setter(TranslateTransform.YProperty, 0.0) },
KeyTime = TimeSpan.FromMilliseconds(600)
}
}
};
await Task.WhenAll(
fadeInAnimation.RunAsync(contentGrid),
slideUpAnimation.RunAsync(translateTransform));
Console.WriteLine("[OobeWindow] Entrance animation completed");
typingTextBlock.Text = fullText.Substring(0, i);
await Task.Delay(TypingDelayMs);
}
catch (Exception ex)
// 停顿一下
await Task.Delay(500);
// 隐藏光标
cursorBorder.IsVisible = false;
// 显示副标题(打字机效果:下一代 互动信息看板)
if (subtitlePanel != null)
{
Console.Error.WriteLine($"[OobeWindow] Error playing entrance animation: {ex.Message}");
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);
}
}
public Task WaitForEnterAsync() => _completionSource.Task;
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 = "下一代";
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 = "互动信息看板";
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, _migrateExistingData);
}
await NavigateToStep(4);
}
private async void OnEnterClick(object? sender, RoutedEventArgs e)
{
if (_isTransitioning)
{
return;
}
if (_isTransitioning) return;
_isTransitioning = true;
Console.WriteLine("[OobeWindow] Enter button clicked, starting transition...");
try
{
await PlayExitAnimationAsync();
_completionSource.TrySetResult(true);
Close();
}
catch (Exception ex)
{
Console.Error.WriteLine($"[OobeWindow] Error during transition: {ex.Message}");
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 = "检测到现有数据,选择便携模式时将自动迁移。";
}
}
}
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>("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>("WelcomeStep"),
_ => null
};
if (currentStepControl == null || nextStepControl == null)
{
_isTransitioning = false;
return;
}
await AnimateOpacityAsync(currentStepControl, 1, 0, AnimationDurationMs);
currentStepControl.IsVisible = false;
nextStepControl.IsVisible = true;
nextStepControl.Opacity = 0;
await AnimateOpacityAsync(nextStepControl, 0, 1, AnimationDurationMs);
_currentStep = step;
_isTransitioning = false;
}
private async Task PlayExitAnimationAsync()
{
try
var contentGrid = this.FindControl<Grid>("ContentGrid");
if (contentGrid != null)
{
var contentGrid = this.FindControl<Grid>("ContentGrid");
if (contentGrid is null)
{
await Task.Delay(200);
return;
}
var fadeOutAnimation = new Animation
{
Duration = TimeSpan.FromMilliseconds(200),
Easing = new CubicEaseIn(),
Children =
{
new KeyFrame
{
Setters = { new Setter(OpacityProperty, 1.0) },
KeyTime = TimeSpan.FromMilliseconds(0)
},
new KeyFrame
{
Setters = { new Setter(OpacityProperty, 0.0) },
KeyTime = TimeSpan.FromMilliseconds(200)
}
}
};
await fadeOutAnimation.RunAsync(contentGrid);
Console.WriteLine("[OobeWindow] Exit animation completed");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[OobeWindow] Error playing exit animation: {ex.Message}");
await AnimateOpacityAsync(contentGrid, 1, 0, AnimationDurationMs);
}
}
private double ResolveEntranceOffset()
private static async Task AnimateOpacityAsync(Control element, double from, double to, int durationMs)
{
var boundsHeight = Bounds.Height > 0 ? Bounds.Height : Height;
var scaledOffset = boundsHeight * 0.05;
return Math.Clamp(scaledOffset, 20, 48);
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);
}
}
// 枚举定义(使用 Services 命名空间中的 ThemeMode
public enum MonetSource
{
Wallpaper,
Custom,
Disabled
}