diff --git a/.trae/documents/skeleton-screen-plan.md b/.trae/documents/skeleton-screen-plan.md new file mode 100644 index 0000000..b5d4828 --- /dev/null +++ b/.trae/documents/skeleton-screen-plan.md @@ -0,0 +1,117 @@ +# 骨架页(Skeleton Screen)实施计划 + +## 问题分析 + +当前首次启动时,用户看到的现象: + +1. 全屏窗口立即显示,但状态栏组件全部 `IsVisible="False"`(空白) +2. 头像区域只有 fallback 文字 "U",尺寸未计算(显得巨大) +3. 底部 Dock 任务栏已经渲染但内容为空 +4. 壁纸加载完成前,桌面区域是透明/黑色的 +5. 整体看起来就是一个"Dock 栏覆盖全屏 + 巨大头像"的半成品状态 + +**根本原因**:`OnOpened` 中有大量同步初始化操作(壁纸解码、组件布局、启动器扫描等),在它们完成之前,UI 元素要么不可见要么处于默认状态。 + +## 方案概述 + +在 `DesktopPage` 层添加一个**骨架遮罩层**,覆盖在真实内容之上,在初始化完成前显示骨架占位,初始化完成后淡出消失。 + +### 骨架页布局 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ [状态栏骨架] │ +│ ┌─────────┐ ┌──────────────────┐ ┌─────────┐ │ +│ │ ○ 头像 │ │ ████ 时钟 ████ │ │ ○ ○ │ │ +│ └─────────┘ └──────────────────┘ └─────────┘ │ +│ │ +│ (桌面区域 - 壁纸层) │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ [Dock 任务栏骨架] │ │ +│ │ ██ ████████ ████████ ████████ ████████ ○ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ +``` + +### 骨架元素 + +| 区域 | 骨架元素 | 形状 | 说明 | +| ------- | ------ | ----------------------------------- | ----------------------------- | +| 状态栏-中间 | 时钟骨架 | 圆角矩形(`DesignCornerRadiusComponent`) | 模拟 ClockWidget 的胶囊形状 | +| 状态栏-中间 | 文本胶囊骨架 | 圆角矩形(较小) | 模拟 TextCapsuleWidget | +| 底部 Dock | 头像骨架 | 圆形 | 模拟 TaskbarProfileAvatarBorder | +| 底部 Dock | 操作按钮骨架 | 圆角矩形 | 模拟任务栏按钮 | +| 底部 Dock | 分隔线骨架 | 细长矩形 | 模拟按钮间分隔 | + +### 骨架样式 + +* **颜色**:使用 `AdaptiveGlassPanelBackgroundBrush` 作为基础色,叠加一个 **Shimmer 动画**(微光扫过效果) + +* **圆角**:与真实组件一致,使用 `DesignCornerRadiusComponent` + +* **动画**:Shimmer 微光从左到右扫过,周期 2s,使用 `FluttermotionToken` 缓动 + +## 实施步骤 + +### Step 1: 创建 Shimmer 动画画刷 + +在 `GlassModule.axaml` 或新建 `SkeletonStyles.axaml` 中定义: + +* 创建 `ShimmerBrush`:一个 `LinearGradientBrush`,包含高光条带 + +* 创建 `ShimmerAnimation` storyboard:让高光条带从左到右移动 + +* 定义 `skeleton-shimmer` 样式类:应用 ShimmerBrush + 动画 + +### Step 2: 在 MainWindow\.axaml 中添加骨架遮罩层 + +在 `DesktopPage` Grid 内、所有真实内容之上添加一个 `Grid x:Name="SkeletonOverlay"`: + +* 初始 `IsVisible="True"`,`ZIndex="999"` + +* 包含状态栏骨架区域和底部 Dock 骨架区域 + +* 使用与真实布局相同的 Grid RowDefinitions,确保骨架元素对齐 + +### Step 3: 在 MainWindow\.axaml.cs 中控制骨架显示/隐藏 + +* 在 `OnOpened` 开始时,骨架层可见 + +* 在 `OnOpened` 末尾(所有初始化完成后),调用 `HideSkeletonOverlayAsync()` + +* `HideSkeletonOverlayAsync()`:播放淡出动画 → 设置 `IsVisible="False"` + +* 如果启用了滑入滑出过渡,骨架层在入场动画期间也应可见,入场动画完成后再淡出 + +### Step 4: 骨架元素尺寸适配 + +* 骨架元素需要在 `ApplyTaskbarSettings()` 后更新尺寸(因为 `taskbarCellHeight` 等值在 OnOpened 中才计算) + +* 或者在 XAML 中使用相对尺寸(百分比/比例),避免依赖代码计算 + +### Step 5: 与窗口过渡动画的协调 + +* 入场动画(`PrepareEnterAnimation` / `PlayEnterAnimation`)期间,骨架层应保持可见 + +* 入场动画完成后,先短暂显示骨架(\~100ms),然后淡出骨架 + +* 退场动画时,无需特殊处理(骨架已隐藏) + +## 涉及文件 + +| 文件 | 改动类型 | +| --------------------------------- | -------------------- | +| `Styles/SkeletonStyles.axaml`(新建) | Shimmer 画刷 + 骨架样式类 | +| `Views/MainWindow.axaml` | 添加 SkeletonOverlay 层 | +| `Views/MainWindow.axaml.cs` | 骨架显示/隐藏逻辑 | +| `App.axaml` | 引入 SkeletonStyles 资源 | + +## 不涉及的文件 + +* 不修改组件代码(ClockWidget、TextCapsuleWidget 等) + +* 不修改设置系统 + +* 不修改 App.axaml.cs 的启动流程 + diff --git a/.trae/specs/window-slide-transition/checklist.md b/.trae/specs/window-slide-transition/checklist.md new file mode 100644 index 0000000..3c06ced --- /dev/null +++ b/.trae/specs/window-slide-transition/checklist.md @@ -0,0 +1,24 @@ +* [x] AppSettingsSnapshot 包含 EnableSlideTransition 字段且默认为 false + +* [x] DesktopPage 拥有名为 DesktopPageSlideTransform 的 TranslateTransform + +* [x] DesktopPage.Transitions 包含 Opacity 和 TranslateTransform.X 两个 DoubleTransition + +* [x] 点击"回到 Windows"时播放退场动画(Opacity 淡出 或 Opacity+滑动),动画完成后再最小化 + +* [x] 从最小化恢复时 DesktopPage 先以 Opacity=0 遮住 Normal 中间态,FullScreen 生效后播放入场动画 + +* [x] 动画期间 DesktopPage.IsHitTestVisible 为 false,动画完成后恢复 + +* [x] 动画期间 OnWindowPropertyChanged 不执行强制全屏纠正 + +* [x] 快速连续操作不会导致动画冲突 + +* [x] GeneralSettingsPage 在 Windows 平台显示"滑入滑出过渡效果"开关 + +* [x] GeneralSettingsPage 在非 Windows 平台不显示该开关 + +* [x] EnableSlideTransition 设置持久化到 AppSettingsSnapshot 且立即生效 + +* [x] dotnet build 无编译错误 + diff --git a/.trae/specs/window-slide-transition/spec.md b/.trae/specs/window-slide-transition/spec.md new file mode 100644 index 0000000..f7b8ea3 --- /dev/null +++ b/.trae/specs/window-slide-transition/spec.md @@ -0,0 +1,138 @@ +# 窗口过渡动画 Spec + +## Why + +当前全屏窗口在"回到 Windows"(最小化)和"恢复应用"时存在严重的视觉问题: +1. 恢复时经历 `Minimized → Normal → FullScreen` 两步跳变,用户会短暂看到无框小窗口 +2. 状态切换无任何过渡动画,体验生硬 +3. `OnWindowPropertyChanged` 使用 `Dispatcher.UIThread.Post` 延迟纠正,进一步延长了 Normal 中间态的可见时间 + +## What Changes + +- 在 `MainWindow.axaml` 的 `DesktopPage` 上添加 `TranslateTransform` 和 `TranslateTransform.X` 过渡动画 +- 修改 `MainWindow.axaml.cs` 的 `OnMinimizeClick`,实现退场动画(滑出/淡出 → 最小化) +- 修改 `App.axaml.cs` 的 `RestoreOrCreateMainWindow`,实现入场动画(全屏 → 滑入/淡入) +- 修改 `MainWindow.axaml.cs` 的 `OnWindowPropertyChanged`,在动画期间暂停强制全屏逻辑 +- 在 `AppSettingsSnapshot` 中添加 `EnableSlideTransition` 设置项(默认关闭) +- 在 `GeneralSettingsPageViewModel` 中添加对应 ViewModel 属性 +- 在 `GeneralSettingsPage.axaml` 中添加开关 UI(仅 Windows 平台显示) +- 添加平台检测逻辑:Windows 且开启设置时使用滑入滑出,其他情况使用 Opacity 淡入淡出 + +## Impact + +- Affected specs: 窗口生命周期过渡动画 +- Affected code: + - `LanMountainDesktop/Views/MainWindow.axaml` - DesktopPage 添加 TranslateTransform + - `LanMountainDesktop/Views/MainWindow.axaml.cs` - OnMinimizeClick、OnWindowPropertyChanged、新增动画方法 + - `LanMountainDesktop/App.axaml.cs` - RestoreOrCreateMainWindow、OnMainWindowPropertyChanged + - `LanMountainDesktop/Models/AppSettingsSnapshot.cs` - 新增 EnableSlideTransition 字段 + - `LanMountainDesktop/ViewModels/SettingsViewModels.cs` - GeneralSettingsPageViewModel 新增属性 + - `LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml` - 新增开关 UI + +--- + +## ADDED Requirements + +### Requirement: 窗口退场过渡动画 + +系统 SHALL 在主窗口最小化/隐藏时播放退场过渡动画,消除窗口状态跳变的视觉闪烁。 + +#### Scenario: Opacity 淡出退场(所有平台默认) +- **WHEN** 用户点击"回到 Windows"或触发最小化 +- **THEN** 系统将 `DesktopPage.Opacity` 设为 0,触发淡出动画 +- **AND THEN** 动画完成后执行 `WindowState = Minimized` +- **AND THEN** 最小化完成后重置 `DesktopPage.Opacity = 1`(窗口已不可见) + +#### Scenario: 滑出退场(Windows + 开启设置) +- **WHEN** 用户点击"回到 Windows"且运行在 Windows 平台且已开启滑入滑出设置 +- **THEN** 系统同时将 `DesktopPage.Opacity` 设为 0 且 `DesktopPageSlideTransform.X` 设为屏幕宽度 +- **AND THEN** 动画完成后执行 `WindowState = Minimized` +- **AND THEN** 最小化完成后重置 `DesktopPageSlideTransform.X = 0` 和 `DesktopPage.Opacity = 1` + +### Requirement: 窗口入场过渡动画 + +系统 SHALL 在主窗口恢复时播放入场过渡动画,消除 Normal 中间态的视觉闪烁。 + +#### Scenario: Opacity 淡入入场(所有平台默认) +- **WHEN** 主窗口从最小化/隐藏状态恢复 +- **THEN** 系统先将 `DesktopPage.Opacity` 设为 0(遮住 Normal 中间态) +- **AND THEN** 完成 `Minimized → Normal → FullScreen` 状态切换 +- **AND THEN** 等 FullScreen 状态生效后将 `DesktopPage.Opacity` 设为 1,触发淡入动画 + +#### Scenario: 滑入入场(Windows + 开启设置) +- **WHEN** 主窗口从最小化/隐藏状态恢复且运行在 Windows 平台且已开启滑入滑出设置 +- **THEN** 系统先将 `DesktopPage.Opacity` 设为 0 且 `DesktopPageSlideTransform.X` 设为屏幕宽度 +- **AND THEN** 完成 `Minimized → Normal → FullScreen` 状态切换 +- **AND THEN** 等 FullScreen 状态生效后同时将 `DesktopPage.Opacity` 设为 1 且 `DesktopPageSlideTransform.X` 设为 0,触发滑入+淡入组合动画 + +### Requirement: 动画期间交互保护 + +系统 SHALL 在过渡动画播放期间防止用户交互和状态冲突。 + +#### Scenario: 动画期间禁止交互 +- **WHEN** 退场或入场动画正在播放 +- **THEN** `DesktopPage.IsHitTestVisible` 设为 `false` +- **AND THEN** 动画完成后恢复为 `true` + +#### Scenario: 动画期间暂停强制全屏 +- **WHEN** 入场动画正在播放且窗口临时处于 Normal 状态 +- **THEN** `OnWindowPropertyChanged` 不执行强制全屏纠正 +- **AND THEN** 入场动画完成后恢复正常强制全屏逻辑 + +#### Scenario: 防止快速连续操作 +- **WHEN** 用户在动画播放期间再次触发最小化或恢复 +- **THEN** 系统忽略重复操作,避免动画冲突 + +### Requirement: 滑入滑出设置项 + +系统 SHALL 在基本设置页面提供"滑入滑出过渡效果"开关,仅 Windows 平台可见。 + +#### Scenario: 设置项可见性 +- **WHEN** 用户在 Windows 平台打开基本设置页面 +- **THEN** 显示"滑入滑出过渡效果"开关 +- **WHEN** 用户在非 Windows 平台打开基本设置页面 +- **THEN** 不显示该开关 + +#### Scenario: 设置项默认值 +- **WHEN** 用户首次安装应用 +- **THEN** `EnableSlideTransition` 默认为 `false` + +#### Scenario: 设置持久化 +- **WHEN** 用户切换"滑入滑出过渡效果"开关 +- **THEN** 设置值立即持久化到 `AppSettingsSnapshot.EnableSlideTransition` +- **AND THEN** 下次窗口过渡时立即生效,无需重启 + +### Requirement: DesktopPage TranslateTransform 声明 + +系统 SHALL 在 `DesktopPage` 上声明 `TranslateTransform` 和对应的过渡动画。 + +#### Scenario: XAML 声明 +- **WHEN** MainWindow 初始化 +- **THEN** `DesktopPage` 拥有名为 `DesktopPageSlideTransform` 的 `TranslateTransform` +- **AND THEN** `DesktopPage.Transitions` 包含 `Opacity` 和 `TranslateTransform.X` 两个过渡 +- **AND THEN** 过渡时长使用 `FluttermotionToken.Duration.Page`(320ms)和 `FluttermotionToken.Duration.Intro`(400ms) +- **AND THEN** 缓动函数使用 `0.05,0.75,0.10,1.00`(DecelerateBezier) + +## MODIFIED Requirements + +### Requirement: OnMinimizeClick 行为 + +**当前**: 直接设置 `WindowState = WindowState.Minimized`,无动画 + +**修改后**: 先播放退场动画,动画完成后再设置 `WindowState = WindowState.Minimized` + +### Requirement: RestoreOrCreateMainWindow 行为 + +**当前**: `Show() → Normal → FullScreen`,无过渡动画,用户可见 Normal 中间态 + +**修改后**: 先将 `DesktopPage` 设为不可见(Opacity=0 + 可选滑出位),再执行状态切换,最后播放入场动画 + +### Requirement: OnWindowPropertyChanged 强制全屏逻辑 + +**当前**: 任何非 Minimized/FullScreen 状态立即纠正为 FullScreen + +**修改后**: 动画期间允许临时 Normal 状态存在,动画完成后恢复强制全屏逻辑 + +## REMOVED Requirements + +无移除的需求。 diff --git a/.trae/specs/window-slide-transition/tasks.md b/.trae/specs/window-slide-transition/tasks.md new file mode 100644 index 0000000..b95c94c --- /dev/null +++ b/.trae/specs/window-slide-transition/tasks.md @@ -0,0 +1,52 @@ +# Tasks + +- [x] Task 1: 在 `AppSettingsSnapshot` 中添加 `EnableSlideTransition` 字段 + - [x] 添加 `public bool EnableSlideTransition { get; set; } = false;` + - [x] 在 `Clone()` 方法中无需特殊处理(bool 是值类型) + +- [x] Task 2: 在 `MainWindow.axaml` 的 `DesktopPage` 上添加 `TranslateTransform` 和过渡动画 + - [x] 添加 `` + - [x] 在 `Grid.Transitions` 中添加 `TranslateTransform.X` 的 `DoubleTransition`,使用 `FluttermotionToken.Duration.Intro` 和 DecelerateBezier 缓动 + +- [x] Task 3: 在 `MainWindow.axaml.cs` 中实现退场动画逻辑 + - [x] 添加 `_isSlideAnimationActive` 标志位 + - [x] 修改 `OnMinimizeClick`,调用新的 `SlideOutAndMinimizeAsync` 方法 + - [x] 实现 `SlideOutAndMinimizeAsync`:读取设置 → 播放退场动画(Opacity + 可选滑动)→ 等动画完成 → 最小化 → 重置位置 + - [x] 动画期间设置 `DesktopPage.IsHitTestVisible = false` + +- [x] Task 4: 在 `MainWindow.axaml.cs` 中实现入场动画逻辑 + - [x] 添加 `public void PrepareEnterAnimation()` 方法:禁用过渡 → 设置初始位置(Opacity=0, X=屏幕宽度或0)→ 重新启用过渡 + - [x] 添加 `public void PlayEnterAnimation()` 方法:触发入场动画(Opacity=1, X=0) + - [x] 添加 `private bool IsSlideTransitionEnabled()` 方法,从设置中读取 + +- [x] Task 5: 修改 `App.axaml.cs` 的 `RestoreOrCreateMainWindow` + - [x] 在窗口状态切换前调用 `mainWindow.PrepareEnterAnimation()` + - [x] 在 FullScreen 状态生效后调用 `mainWindow.PlayEnterAnimation()` + +- [x] Task 6: 修改 `MainWindow.axaml.cs` 的 `OnWindowPropertyChanged` + - [x] 当 `_isSlideAnimationActive` 为 true 时跳过强制全屏逻辑 + +- [x] Task 7: 在 `GeneralSettingsPageViewModel` 中添加 `EnableSlideTransition` 属性 + - [x] 添加 `[ObservableProperty] private bool _enableSlideTransition;` + - [x] 添加 `OnEnableSlideTransitionChanged` 持久化方法 + - [x] 在构造函数和 `OnSettingsChanged` 中加载/同步该设置 + - [x] 添加 `IsSlideTransitionAvailable` 平台检测属性 + +- [x] Task 8: 在 `GeneralSettingsPage.axaml` 中添加"滑入滑出过渡效果"开关 + - [x] 在"运行时设置"分组中添加 `SettingsExpander` + - [x] 仅 Windows 平台显示(使用 `IsVisible` 绑定到 `IsSlideTransitionAvailable`) + - [x] 图标使用 `ArrowRight` + +- [x] Task 9: 构建验证 + - [x] 执行 `dotnet build` 确保无编译错误 + +# Task Dependencies + +- [Task 2] depends on [Task 1] +- [Task 3] depends on [Task 1, Task 2] +- [Task 4] depends on [Task 1, Task 2] +- [Task 5] depends on [Task 4] +- [Task 6] depends on [Task 3] +- [Task 7] depends on [Task 1] +- [Task 8] depends on [Task 7] +- [Task 9] depends on [Task 3, Task 4, Task 5, Task 6, Task 7, Task 8] diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 92abfc4..e60e7cd 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -566,13 +566,14 @@ public partial class App : Application try { - // 先隐藏透明覆盖层窗口 if (_transparentOverlayWindow is not null && _transparentOverlayWindow.IsVisible) { _transparentOverlayWindow.Hide(); } - + var mainWindow = GetOrCreateMainWindow(desktop, source); + mainWindow.PrepareEnterAnimation(); + mainWindow.ShowInTaskbar = true; if (!mainWindow.IsVisible) @@ -593,6 +594,12 @@ public partial class App : Application mainWindow.Activate(); mainWindow.Topmost = true; mainWindow.Topmost = false; + + Dispatcher.UIThread.Post(() => + { + mainWindow.PlayEnterAnimation(); + }, DispatcherPriority.Background); + SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}"); AppLogger.Info( "DesktopShell", diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs index f1d61c4..c487059 100644 --- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs @@ -152,6 +152,8 @@ public sealed class AppSettingsSnapshot public bool EnableThreeFingerSwipe { get; set; } = false; + public bool EnableSlideTransition { get; set; } = false; + public bool EnableFusedDesktop { get; set; } = false; public List DisabledPluginIds { get; set; } = []; diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs index b006eab..2f2b124 100644 --- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs +++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs @@ -201,6 +201,7 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo SelectedRenderMode = RenderModes.FirstOrDefault(option => string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase)) ?? RenderModes[0]; + EnableSlideTransition = appSnapshot.EnableSlideTransition; _isInitializing = false; RefreshPreview(); @@ -232,6 +233,11 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo { return; } + + if (changedKeys.Contains(nameof(AppSettingsSnapshot.EnableSlideTransition))) + { + EnableSlideTransition = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App).EnableSlideTransition; + } } public event Action? RestartRequested; @@ -251,6 +257,11 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo [ObservableProperty] private SelectionOption _selectedRenderMode = new(AppRenderingModeHelper.Default, "Default"); + [ObservableProperty] + private bool _enableSlideTransition; + + public bool IsSlideTransitionAvailable => System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows); + [ObservableProperty] private string _pageTitle = string.Empty; @@ -350,6 +361,24 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo } } + partial void OnEnableSlideTransitionChanged(bool value) + { + if (_isInitializing) return; + SaveField(nameof(AppSettingsSnapshot.EnableSlideTransition), value); + } + + private void SaveField(string key, T value) + { + var snapshot = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App); + var property = typeof(AppSettingsSnapshot).GetProperty(key); + if (property is not null && property.CanWrite) + { + property.SetValue(snapshot, value); + } + + _settingsFacade.Settings.SaveSnapshot(SettingsScope.App, snapshot, changedKeys: [key]); + } + private IReadOnlyList CreateLanguageOptions() { return diff --git a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs index bede200..03ffa8f 100644 --- a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs +++ b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs @@ -77,7 +77,9 @@ public partial class MainWindow string.Equals(key, nameof(AppSettingsSnapshot.UpdateChannel), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.UpdateMode), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadSource), StringComparison.OrdinalIgnoreCase) || - string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadThreads), StringComparison.OrdinalIgnoreCase))) + string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadThreads), StringComparison.OrdinalIgnoreCase) || + string.Equals(key, nameof(AppSettingsSnapshot.EnableThreeFingerSwipe), StringComparison.OrdinalIgnoreCase) || + string.Equals(key, nameof(AppSettingsSnapshot.EnableSlideTransition), StringComparison.OrdinalIgnoreCase))) { return; } diff --git a/LanMountainDesktop/Views/MainWindow.axaml b/LanMountainDesktop/Views/MainWindow.axaml index a9847ef..c9b00df 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml +++ b/LanMountainDesktop/Views/MainWindow.axaml @@ -98,9 +98,13 @@ + + + - + + diff --git a/LanMountainDesktop/Views/MainWindow.axaml.cs b/LanMountainDesktop/Views/MainWindow.axaml.cs index 75e15c5..b56a297 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml.cs +++ b/LanMountainDesktop/Views/MainWindow.axaml.cs @@ -132,6 +132,8 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider private double _currentDesktopCellGap; private double _currentDesktopEdgeInset; private string _gridSpacingPreset = "Relaxed"; + private bool _isSlideAnimationActive; + private TranslateTransform? _desktopPageSlideTransform; private string _statusBarSpacingMode = "Relaxed"; private int _statusBarCustomSpacingPercent = 12; private bool _statusBarClockTransparentBackground; @@ -833,7 +835,103 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider private void OnMinimizeClick(object? sender, RoutedEventArgs e) { + if (_isSlideAnimationActive) + { + return; + } + + SlideOutAndMinimizeAsync(); + } + + private TranslateTransform GetDesktopPageSlideTransform() + { + if (_desktopPageSlideTransform is not null) + { + return _desktopPageSlideTransform; + } + + _desktopPageSlideTransform = DesktopPage.RenderTransform as TranslateTransform; + if (_desktopPageSlideTransform is null) + { + _desktopPageSlideTransform = new TranslateTransform(); + DesktopPage.RenderTransform = _desktopPageSlideTransform; + } + + return _desktopPageSlideTransform; + } + + private async void SlideOutAndMinimizeAsync() + { + _isSlideAnimationActive = true; + DesktopPage.IsHitTestVisible = false; + + var useSlide = IsSlideTransitionEnabled(); + var slideTransform = GetDesktopPageSlideTransform(); + + if (useSlide) + { + slideTransform.X = Bounds.Width; + } + + DesktopPage.Opacity = 0; + + await Task.Delay(useSlide + ? FluttermotionToken.Intro + : FluttermotionToken.Page); + + if (!_isSlideAnimationActive) + { + return; + } + WindowState = WindowState.Minimized; + + slideTransform.X = 0; + DesktopPage.Opacity = 1; + DesktopPage.IsHitTestVisible = true; + _isSlideAnimationActive = false; + } + + public void PrepareEnterAnimation() + { + _isSlideAnimationActive = false; + + var useSlide = IsSlideTransitionEnabled(); + var slideTransform = GetDesktopPageSlideTransform(); + + var savedTransitions = DesktopPage.Transitions; + DesktopPage.Transitions = null; + + DesktopPage.Opacity = 0; + + if (useSlide) + { + slideTransform.X = Bounds.Width > 0 ? Bounds.Width : 1920; + } + + DesktopPage.Transitions = savedTransitions; + DesktopPage.IsHitTestVisible = false; + _isSlideAnimationActive = true; + } + + public void PlayEnterAnimation() + { + var slideTransform = GetDesktopPageSlideTransform(); + DesktopPage.Opacity = 1; + slideTransform.X = 0; + DesktopPage.IsHitTestVisible = true; + _isSlideAnimationActive = false; + } + + private bool IsSlideTransitionEnabled() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return false; + } + + var snapshot = _settingsService.LoadSnapshot(SettingsScope.App); + return snapshot.EnableSlideTransition; } private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) @@ -848,8 +946,18 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider return; } + if (_isSlideAnimationActive) + { + return; + } + Dispatcher.UIThread.Post(() => { + if (_isSlideAnimationActive) + { + return; + } + if (WindowState is not (WindowState.Minimized or WindowState.FullScreen)) { WindowState = WindowState.FullScreen; diff --git a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml index e192389..67108f9 100644 --- a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml @@ -106,6 +106,17 @@ + + + + + + + + +