mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03e32ee6cb | ||
|
|
c2cc62b58b | ||
|
|
9c529f2992 |
24
.trae/specs/window-slide-transition/checklist.md
Normal file
24
.trae/specs/window-slide-transition/checklist.md
Normal file
@@ -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 无编译错误
|
||||||
|
|
||||||
138
.trae/specs/window-slide-transition/spec.md
Normal file
138
.trae/specs/window-slide-transition/spec.md
Normal file
@@ -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
|
||||||
|
|
||||||
|
无移除的需求。
|
||||||
52
.trae/specs/window-slide-transition/tasks.md
Normal file
52
.trae/specs/window-slide-transition/tasks.md
Normal file
@@ -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] 添加 `<TranslateTransform />`
|
||||||
|
- [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]
|
||||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -36,6 +36,28 @@
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
## [0.8.3.4](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.4) - 2026-04-12
|
||||||
|
|
||||||
|
### 新增 (Added)
|
||||||
|
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 变更 (Changed)
|
||||||
|
|
||||||
|
- ♻️ **插件 SDK 更新**: 更新插件 SDK,优化插件开发接口和兼容性
|
||||||
|
|
||||||
|
### 修复 (Fixed)
|
||||||
|
|
||||||
|
- 🐛 **轻量版 .NET 依赖问题(实验性)**: 实验性修复了轻量版在 .NET 环境下的依赖问题
|
||||||
|
- 问题原因: 轻量版与 .NET 的依赖兼容性存在冲突
|
||||||
|
- 修复方案: 调整依赖配置,提升兼容性(实验性修复,持续观察中)
|
||||||
|
|
||||||
|
### 移除 (Removed)
|
||||||
|
|
||||||
|
- 无
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
## [0.8.3.3](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.3) - 2026-04-12
|
## [0.8.3.3](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.3) - 2026-04-12
|
||||||
|
|
||||||
### 新增 (Added)
|
### 新增 (Added)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>4.0.1</Version>
|
<Version>4.0.2</Version>
|
||||||
<PackageId>LanMountainDesktop.PluginSdk</PackageId>
|
<PackageId>LanMountainDesktop.PluginSdk</PackageId>
|
||||||
<IsPackable>true</IsPackable>
|
<IsPackable>true</IsPackable>
|
||||||
<Authors>LanMountainDesktop</Authors>
|
<Authors>LanMountainDesktop</Authors>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ namespace LanMountainDesktop.PluginSdk;
|
|||||||
|
|
||||||
public static class PluginSdkInfo
|
public static class PluginSdkInfo
|
||||||
{
|
{
|
||||||
public const string ApiVersion = "4.0.1";
|
public const string ApiVersion = "4.0.2";
|
||||||
public const string ManifestFileName = "plugin.json";
|
public const string ManifestFileName = "plugin.json";
|
||||||
public const string PackageFileExtension = ".laapp";
|
public const string PackageFileExtension = ".laapp";
|
||||||
public const string DataDirectoryName = "Data";
|
public const string DataDirectoryName = "Data";
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
"pluginSdkVersion": {
|
"pluginSdkVersion": {
|
||||||
"type": "parameter",
|
"type": "parameter",
|
||||||
"datatype": "text",
|
"datatype": "text",
|
||||||
"defaultValue": "4.0.0",
|
"defaultValue": "4.0.2",
|
||||||
"description": "LanMountainDesktop.PluginSdk package version.",
|
"description": "LanMountainDesktop.PluginSdk package version.",
|
||||||
"replaces": "__PLUGIN_SDK_VERSION__"
|
"replaces": "__PLUGIN_SDK_VERSION__"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"description": "__PLUGIN_DESCRIPTION__",
|
"description": "__PLUGIN_DESCRIPTION__",
|
||||||
"author": "__PLUGIN_AUTHOR__",
|
"author": "__PLUGIN_AUTHOR__",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"apiVersion": "4.0.1",
|
"apiVersion": "4.0.2",
|
||||||
"entranceAssembly": "LanMountainDesktop.PluginTemplate.dll",
|
"entranceAssembly": "LanMountainDesktop.PluginTemplate.dll",
|
||||||
"sharedContracts": []
|
"sharedContracts": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -566,13 +566,14 @@ public partial class App : Application
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 先隐藏透明覆盖层窗口
|
|
||||||
if (_transparentOverlayWindow is not null && _transparentOverlayWindow.IsVisible)
|
if (_transparentOverlayWindow is not null && _transparentOverlayWindow.IsVisible)
|
||||||
{
|
{
|
||||||
_transparentOverlayWindow.Hide();
|
_transparentOverlayWindow.Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainWindow = GetOrCreateMainWindow(desktop, source);
|
var mainWindow = GetOrCreateMainWindow(desktop, source);
|
||||||
|
mainWindow.PrepareEnterAnimation();
|
||||||
|
|
||||||
mainWindow.ShowInTaskbar = true;
|
mainWindow.ShowInTaskbar = true;
|
||||||
|
|
||||||
if (!mainWindow.IsVisible)
|
if (!mainWindow.IsVisible)
|
||||||
@@ -593,6 +594,12 @@ public partial class App : Application
|
|||||||
mainWindow.Activate();
|
mainWindow.Activate();
|
||||||
mainWindow.Topmost = true;
|
mainWindow.Topmost = true;
|
||||||
mainWindow.Topmost = false;
|
mainWindow.Topmost = false;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
mainWindow.PlayEnterAnimation();
|
||||||
|
}, DispatcherPriority.Background);
|
||||||
|
|
||||||
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}");
|
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}");
|
||||||
AppLogger.Info(
|
AppLogger.Info(
|
||||||
"DesktopShell",
|
"DesktopShell",
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ public sealed class AppSettingsSnapshot
|
|||||||
|
|
||||||
public bool EnableThreeFingerSwipe { get; set; } = false;
|
public bool EnableThreeFingerSwipe { get; set; } = false;
|
||||||
|
|
||||||
|
public bool EnableSlideTransition { get; set; } = false;
|
||||||
|
|
||||||
public bool EnableFusedDesktop { get; set; } = false;
|
public bool EnableFusedDesktop { get; set; } = false;
|
||||||
|
|
||||||
public List<string> DisabledPluginIds { get; set; } = [];
|
public List<string> DisabledPluginIds { get; set; } = [];
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
|
|||||||
SelectedRenderMode = RenderModes.FirstOrDefault(option =>
|
SelectedRenderMode = RenderModes.FirstOrDefault(option =>
|
||||||
string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase))
|
string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase))
|
||||||
?? RenderModes[0];
|
?? RenderModes[0];
|
||||||
|
EnableSlideTransition = appSnapshot.EnableSlideTransition;
|
||||||
_isInitializing = false;
|
_isInitializing = false;
|
||||||
|
|
||||||
RefreshPreview();
|
RefreshPreview();
|
||||||
@@ -232,6 +233,11 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changedKeys.Contains(nameof(AppSettingsSnapshot.EnableSlideTransition)))
|
||||||
|
{
|
||||||
|
EnableSlideTransition = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).EnableSlideTransition;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public event Action? RestartRequested;
|
public event Action? RestartRequested;
|
||||||
@@ -251,6 +257,11 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private SelectionOption _selectedRenderMode = new(AppRenderingModeHelper.Default, "Default");
|
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]
|
[ObservableProperty]
|
||||||
private string _pageTitle = string.Empty;
|
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<T>(string key, T value)
|
||||||
|
{
|
||||||
|
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(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<SelectionOption> CreateLanguageOptions()
|
private IReadOnlyList<SelectionOption> CreateLanguageOptions()
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -30,7 +30,10 @@
|
|||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Margin="2,0,0,0"
|
Margin="2,0,0,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"/>
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||||
|
FontFamily="Consolas, Courier New, monospace"
|
||||||
|
MinWidth="42"
|
||||||
|
TextAlignment="Right"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- 分隔符 -->
|
<!-- 分隔符 -->
|
||||||
@@ -55,7 +58,10 @@
|
|||||||
FontWeight="SemiBold"
|
FontWeight="SemiBold"
|
||||||
Margin="2,0,0,0"
|
Margin="2,0,0,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"/>
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||||
|
FontFamily="Consolas, Courier New, monospace"
|
||||||
|
MinWidth="42"
|
||||||
|
TextAlignment="Right"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- 网络类型图标 -->
|
<!-- 网络类型图标 -->
|
||||||
|
|||||||
@@ -317,31 +317,32 @@ public partial class NetworkSpeedWidget : UserControl, IDesktopComponentWidget
|
|||||||
|
|
||||||
private static string FormatSpeed(long bytesPerSecond)
|
private static string FormatSpeed(long bytesPerSecond)
|
||||||
{
|
{
|
||||||
// 根据数值大小决定显示格式,始终保持3个字符宽度
|
// 根据数值大小选择合适的单位,确保显示始终在1-99.9范围内
|
||||||
// 例如: 1.23, 12.3, 123
|
// 当数值达到100时自动切换到更大的单位
|
||||||
return bytesPerSecond switch
|
return bytesPerSecond switch
|
||||||
{
|
{
|
||||||
>= 1024 * 1024 * 1024 => FormatWithThreeDigits(bytesPerSecond / (1024.0 * 1024.0 * 1024.0), "G"),
|
>= 100L * 1024 * 1024 * 1024 => FormatWithThreeDigits(bytesPerSecond / (1024.0 * 1024.0 * 1024.0), "G"),
|
||||||
>= 1024 * 1024 => FormatWithThreeDigits(bytesPerSecond / (1024.0 * 1024.0), "M"),
|
>= 100L * 1024 * 1024 => FormatWithThreeDigits(bytesPerSecond / (1024.0 * 1024.0), "M"),
|
||||||
>= 1024 => FormatWithThreeDigits(bytesPerSecond / 1024.0, "K"),
|
>= 100L * 1024 => FormatWithThreeDigits(bytesPerSecond / 1024.0, "K"),
|
||||||
|
>= 100 => FormatWithThreeDigits(bytesPerSecond / 1024.0, "K"), // 100B+ 显示为 0.1K
|
||||||
_ => FormatWithThreeDigits(bytesPerSecond, "B")
|
_ => FormatWithThreeDigits(bytesPerSecond, "B")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 格式化数字,始终保持3个有效数字的显示宽度
|
/// 格式化数字,始终保持3位数字+小数点,确保宽度恒定
|
||||||
|
/// 数值范围始终在1-99.9之间
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string FormatWithThreeDigits(double value, string unit)
|
private static string FormatWithThreeDigits(double value, string unit)
|
||||||
{
|
{
|
||||||
// 根据数值大小决定小数位数,确保总宽度一致
|
// 始终保持3位数字,小数点始终存在
|
||||||
|
// 数值范围: 0.0 - 99.9
|
||||||
// < 10: 显示两位小数 (如 1.23)
|
// < 10: 显示两位小数 (如 1.23)
|
||||||
// 10-99: 显示一位小数 (如 12.3)
|
// >= 10: 显示一位小数 (如 12.3, 99.9)
|
||||||
// >= 100: 显示整数 (如 123)
|
|
||||||
string formatted = value switch
|
string formatted = value switch
|
||||||
{
|
{
|
||||||
< 10 => $"{value:F2}",
|
< 10 => $"{value:F2}", // 1.23
|
||||||
< 100 => $"{value:F1}",
|
_ => $"{value:F1}" // 12.3, 99.9
|
||||||
_ => $"{value:F0}"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return formatted + unit;
|
return formatted + unit;
|
||||||
|
|||||||
@@ -77,7 +77,9 @@ public partial class MainWindow
|
|||||||
string.Equals(key, nameof(AppSettingsSnapshot.UpdateChannel), StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(key, nameof(AppSettingsSnapshot.UpdateChannel), StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(key, nameof(AppSettingsSnapshot.UpdateMode), StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(key, nameof(AppSettingsSnapshot.UpdateMode), StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadSource), 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,9 +98,13 @@
|
|||||||
<Grid x:Name="DesktopPage"
|
<Grid x:Name="DesktopPage"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch">
|
VerticalAlignment="Stretch">
|
||||||
|
<Grid.RenderTransform>
|
||||||
|
<TranslateTransform />
|
||||||
|
</Grid.RenderTransform>
|
||||||
<Grid.Transitions>
|
<Grid.Transitions>
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
|
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Page}" Easing="0.05,0.75,0.10,1.00" />
|
||||||
|
<DoubleTransition Property="TranslateTransform.X" Duration="{StaticResource FluttermotionToken.Duration.Intro}" Easing="0.05,0.75,0.10,1.00" />
|
||||||
</Transitions>
|
</Transitions>
|
||||||
</Grid.Transitions>
|
</Grid.Transitions>
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
|||||||
private double _currentDesktopCellGap;
|
private double _currentDesktopCellGap;
|
||||||
private double _currentDesktopEdgeInset;
|
private double _currentDesktopEdgeInset;
|
||||||
private string _gridSpacingPreset = "Relaxed";
|
private string _gridSpacingPreset = "Relaxed";
|
||||||
|
private bool _isSlideAnimationActive;
|
||||||
|
private TranslateTransform? _desktopPageSlideTransform;
|
||||||
private string _statusBarSpacingMode = "Relaxed";
|
private string _statusBarSpacingMode = "Relaxed";
|
||||||
private int _statusBarCustomSpacingPercent = 12;
|
private int _statusBarCustomSpacingPercent = 12;
|
||||||
private bool _statusBarClockTransparentBackground;
|
private bool _statusBarClockTransparentBackground;
|
||||||
@@ -833,7 +835,103 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
|||||||
|
|
||||||
private void OnMinimizeClick(object? sender, RoutedEventArgs e)
|
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;
|
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<AppSettingsSnapshot>(SettingsScope.App);
|
||||||
|
return snapshot.EnableSlideTransition;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||||
@@ -848,8 +946,18 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_isSlideAnimationActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
|
if (_isSlideAnimationActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (WindowState is not (WindowState.Minimized or WindowState.FullScreen))
|
if (WindowState is not (WindowState.Minimized or WindowState.FullScreen))
|
||||||
{
|
{
|
||||||
WindowState = WindowState.FullScreen;
|
WindowState = WindowState.FullScreen;
|
||||||
|
|||||||
@@ -106,6 +106,17 @@
|
|||||||
</ui:SettingsExpanderItem>
|
</ui:SettingsExpanderItem>
|
||||||
</ui:SettingsExpander>
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
<ui:SettingsExpander Header="滑入滑出过渡效果"
|
||||||
|
Description="启用后,进入和退出桌面时使用滑入滑出动画(仅 Windows)"
|
||||||
|
IsVisible="{Binding IsSlideTransitionAvailable}">
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="ArrowRight" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpander.Footer>
|
||||||
|
<ToggleSwitch IsChecked="{Binding EnableSlideTransition}" />
|
||||||
|
</ui:SettingsExpander.Footer>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
Reference in New Issue
Block a user