mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
594a62132f | ||
|
|
15e589aedd | ||
|
|
ac4617f5cf | ||
|
|
0645598753 | ||
|
|
dadd132b4f | ||
|
|
298defb829 | ||
|
|
bcf4be6d50 |
24
.trae/specs/class-schedule-enhancement/checklist.md
Normal file
24
.trae/specs/class-schedule-enhancement/checklist.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Checklist
|
||||
|
||||
## 1. 课表单双周解析修复
|
||||
|
||||
- [x] 单周课程(WeekCountDiv=1)在单周正确显示
|
||||
- [x] 双周课程(WeekCountDiv=2)在双周正确显示
|
||||
- [x] 每周课程(WeekCountDiv=0)在所有周正确显示
|
||||
- [x] 多周轮转(2-32周)正确计算当前周期位置
|
||||
|
||||
## 2. 课程动态移动功能
|
||||
|
||||
- [x] 课程结束自动从视图移除
|
||||
- [x] 新课程自动移入视图可见区域
|
||||
- [x] 当日课程全部结束后自动切换到次日课程表
|
||||
|
||||
## 3. 拖动交互功能
|
||||
|
||||
- [x] 课程表支持上下拖动滚动
|
||||
- [x] 拖动操作流畅、响应及时
|
||||
|
||||
## 4. 自动复位功能
|
||||
|
||||
- [x] 用户手动拖动后,标记拖动状态
|
||||
- [x] 当前课程变化时自动复位到最新进行中课程
|
||||
101
.trae/specs/class-schedule-enhancement/spec.md
Normal file
101
.trae/specs/class-schedule-enhancement/spec.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# 课程表组件功能优化规格说明书
|
||||
|
||||
## Why
|
||||
|
||||
当前课程表组件存在以下问题:
|
||||
1. 单双周课程解析逻辑存在缺陷,无法正确识别单周/双周/每周模式
|
||||
2. 课程无法动态移动,第一列始终显示进行中的课程,但存在无法正常移动的问题
|
||||
3. 缺少用户拖动交互功能
|
||||
4. 缺少拖动后的自动复位机制
|
||||
|
||||
## What Changes
|
||||
|
||||
- 修复 ClassIsland 课程单双周解析逻辑
|
||||
- 实现课程动态移动机制(当前课程结束自动上移)
|
||||
- 实现课程表上下拖动交互功能
|
||||
- 实现自动复位功能(课程结束后视图复位到最新进行中课程)
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected specs
|
||||
- 课程表组件功能规范
|
||||
|
||||
### Affected code
|
||||
- `Services/ClassIslandScheduleDataService.cs` - 课表解析服务
|
||||
- `Views/Components/ClassScheduleWidget.axaml.cs` - 课表组件
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 单双周课程解析
|
||||
|
||||
系统 SHALL 能够正确解析包含单双周信息的课程数据。
|
||||
|
||||
#### Scenario: 单周课程
|
||||
- **WHEN** 课程设置为单周上课
|
||||
- **THEN** 课程仅在单周显示
|
||||
|
||||
#### Scenario: 双周课程
|
||||
- **WHEN** 课程设置为双周上课
|
||||
- **THEN** 课程仅在双周显示
|
||||
|
||||
#### Scenario: 每周课程
|
||||
- **WHEN** 课程设置为每周上课
|
||||
- **THEN** 课程在所有周显示
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 课程动态移动
|
||||
|
||||
系统 SHALL 实现课程的动态移动机制。
|
||||
|
||||
#### Scenario: 课程结束自动上移
|
||||
- **WHEN** 当前进行中的课程结束
|
||||
- **THEN** 课程列表自动向上移动
|
||||
- **AND THEN** 下一个进行中或即将开始的课程移至视图可见区域
|
||||
|
||||
#### Scenario: 新课程移入视图
|
||||
- **WHEN** 新的课程即将开始
|
||||
- **THEN** 该课程自动移至视图可见区域
|
||||
|
||||
#### Scenario: 当日课程全部结束
|
||||
- **WHEN** 当日所有课程已结束
|
||||
- **THEN** 自动显示次日课程表
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 拖动交互功能
|
||||
|
||||
系统 SHALL 提供课程表的上下拖动功能。
|
||||
|
||||
#### Scenario: 拖动查看课程
|
||||
- **WHEN** 用户在课程表区域进行上下拖动
|
||||
- **THEN** 课程列表随拖动方向滚动
|
||||
- **AND THEN** 拖动操作流畅、响应及时
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 自动复位功能
|
||||
|
||||
系统 SHALL 在用户手动拖动后自动复位到当前课程。
|
||||
|
||||
#### Scenario: 当前课程结束触发复位
|
||||
- **WHEN** 用户手动拖动课程表后,当前课程结束
|
||||
- **THEN** 视图自动复位到显示最新进行中课程的位置
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 课程解析逻辑
|
||||
|
||||
**当前**: 单双周解析可能存在缺陷
|
||||
|
||||
**修改后**: 正确识别 WeekCountDiv 和 WeekCountDivTotal 参数,准确判断单周/双周/每周模式
|
||||
|
||||
---
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
(无)
|
||||
61
.trae/specs/class-schedule-enhancement/tasks.md
Normal file
61
.trae/specs/class-schedule-enhancement/tasks.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Tasks
|
||||
|
||||
## 1. 课表单双周解析修复
|
||||
|
||||
- [x] Task 1.1: 分析 ClassIsland 课表单双周数据结构
|
||||
- [x] 分析 ClassIsland Schedule.json 和 Profile.json 中的周数规则字段
|
||||
- [x] 确认 WeekCountDiv 和 WeekCountDivTotal 的含义和取值范围
|
||||
|
||||
- [x] Task 1.2: 修复 GetCyclePositionsByDate 方法
|
||||
- [x] 检查单周开始日期的计算逻辑
|
||||
- [x] 修复周期位置计算公式
|
||||
|
||||
- [x] Task 1.3: 修复 CheckRegularClassPlan 方法
|
||||
- [x] 验证 weekCountDiv 和 weekCountDivTotal 的匹配逻辑
|
||||
- [x] 确保单周=1、双周=2、每周=0 的正确处理
|
||||
|
||||
## 2. 课程动态移动功能
|
||||
|
||||
- [x] Task 2.1: 分析当前课程状态检测逻辑
|
||||
- [x] 查看如何判断课程是否为"当前进行中"
|
||||
|
||||
- [x] Task 2.2: 实现定时刷新机制
|
||||
- [x] 增加更频繁的刷新定时器(每分钟检查一次)
|
||||
- [x] 实现课程状态变化检测
|
||||
|
||||
- [x] Task 2.3: 实现动态移动逻辑
|
||||
- [x] 课程结束后自动上移
|
||||
- [x] 新课程自动移入视图
|
||||
|
||||
- [x] Task 2.4: 实现次日课程切换
|
||||
- [x] 当日所有课程结束后自动切换到次日
|
||||
|
||||
## 3. 拖动交互功能
|
||||
|
||||
- [x] Task 3.1: 实现 ScrollViewer 包裹
|
||||
- [x] 修改 XAML 使用 ScrollViewer 包裹课程列表
|
||||
|
||||
- [x] Task 3.2: 实现拖动手势处理
|
||||
- [x] 添加 PointerPressed/PointerMoved/PointerReleased 处理
|
||||
- [x] 实现平滑滚动逻辑
|
||||
|
||||
## 4. 自动复位功能
|
||||
|
||||
- [x] Task 4.1: 记录用户拖动状态
|
||||
- [x] 添加用户是否手动拖动的标志位
|
||||
|
||||
- [x] Task 4.2: 实现自动复位逻辑
|
||||
- [x] 检测当前课程变化
|
||||
- [x] 当用户手动拖动且当前课程变化时自动复位
|
||||
|
||||
# Task Dependencies
|
||||
|
||||
- Task 1.1 -> Task 1.2 -> Task 1.3
|
||||
- Task 2.1 -> Task 2.2 -> Task 2.3 -> Task 2.4
|
||||
- Task 3.1 -> Task 3.2
|
||||
- Task 4.1 -> Task 4.2
|
||||
|
||||
# Parallelizable Tasks
|
||||
|
||||
- Task 1.x (解析修复) 与 Task 3.x (拖动) 可以并行开发
|
||||
- Task 2.x (动态移动) 可以在 Task 1 完成后进行
|
||||
@@ -0,0 +1,578 @@
|
||||
# 移除视频壁纸功能 - 技术设计文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 设计目标
|
||||
|
||||
本设计文档描述如何从 LanMountainDesktop 项目中完全移除视频壁纸功能,包括:
|
||||
- 移除 LibVLC 相关依赖
|
||||
- 清理主窗口中的视频壁纸代码
|
||||
- 简化壁纸设置页面
|
||||
- 清理本地化资源
|
||||
|
||||
### 1.2 技术约束
|
||||
|
||||
- 保持现有图片壁纸和纯色壁纸功能完整
|
||||
- 确保应用构建和运行正常
|
||||
- 不引入新的外部依赖
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构变更
|
||||
|
||||
### 2.1 变更概览图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 变更前架构 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ MainWindow │
|
||||
│ ├── DesktopWallpaperLayer (背景层) │
|
||||
│ │ ├── DesktopWallpaperImageLayer (图片层) │
|
||||
│ │ ├── DesktopVideoWallpaperImage (视频海报层) │
|
||||
│ │ └── DesktopVideoWallpaperView (VLC视频播放层) │
|
||||
│ ├── _libVlc, _videoWallpaperPlayer, _videoWallpaperMedia │
|
||||
│ └── StartVideoWallpaper(), StopVideoWallpaper() │
|
||||
│ │
|
||||
│ WallpaperSettingsPage │
|
||||
│ ├── 类型选择: Image | Video | SolidColor │
|
||||
│ └── 视频预览区域 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 变更后架构 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ MainWindow │
|
||||
│ ├── DesktopWallpaperLayer (背景层) │
|
||||
│ │ └── DesktopWallpaperImageLayer (图片层) │
|
||||
│ └── (移除所有视频相关字段和方法) │
|
||||
│ │
|
||||
│ WallpaperSettingsPage │
|
||||
│ ├── 类型选择: Image | SolidColor │
|
||||
│ └── (移除视频预览区域) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 组件变更清单
|
||||
|
||||
| 组件 | 变更类型 | 说明 |
|
||||
|------|----------|------|
|
||||
| LanMountainDesktop.csproj | 修改 | 移除 LibVLC 包引用 |
|
||||
| MainWindow.axaml | 修改 | 移除视频控件和命名空间 |
|
||||
| MainWindow.axaml.cs | 修改 | 移除视频相关字段和清理代码 |
|
||||
| MainWindow.SettingsHardCut.Stubs.cs | 修改 | 移除视频壁纸方法 |
|
||||
| AppearanceThemeService.cs | 修改 | 移除视频种子提取器 |
|
||||
| WallpaperSettingsPage.axaml | 修改 | 移除视频类型UI |
|
||||
| WallpaperSettingsPageViewModel.cs | 修改 | 移除视频相关属性 |
|
||||
| SettingsContracts.cs | 修改 | 移除 Video 枚举值 |
|
||||
| SettingsDomainServices.cs | 修改 | 移除视频扩展名检测 |
|
||||
| zh-CN.json | 修改 | 移除视频相关本地化文本 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 详细设计
|
||||
|
||||
### 3.1 项目依赖变更 (LanMountainDesktop.csproj)
|
||||
|
||||
#### 3.1.1 移除的包引用
|
||||
|
||||
```xml
|
||||
<!-- 移除以下包引用 -->
|
||||
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" />
|
||||
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" Condition="..." />
|
||||
<PackageReference Include="VideoLAN.LibVLC.Mac" Version="3.1.3.1" Condition="..." />
|
||||
```
|
||||
|
||||
#### 3.1.2 变更影响
|
||||
|
||||
- 减少约 100MB+ 的依赖包大小
|
||||
- 简化构建和发布流程
|
||||
- 移除平台特定的原生库依赖
|
||||
|
||||
---
|
||||
|
||||
### 3.2 主窗口 XAML 变更 (MainWindow.axaml)
|
||||
|
||||
#### 3.2.1 移除命名空间声明
|
||||
|
||||
```xml
|
||||
<!-- 移除此行 -->
|
||||
xmlns:vlc="clr-namespace:LibVLCSharp.Avalonia;assembly=LibVLCSharp.Avalonia"
|
||||
```
|
||||
|
||||
#### 3.2.2 移除视频壁纸控件
|
||||
|
||||
移除以下控件(约第126-137行):
|
||||
|
||||
```xml
|
||||
<!-- 移除 DesktopVideoWallpaperImage -->
|
||||
<Image x:Name="DesktopVideoWallpaperImage"
|
||||
IsVisible="False"
|
||||
IsHitTestVisible="False"
|
||||
Stretch="UniformToFill"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" />
|
||||
|
||||
<!-- 移除 DesktopVideoWallpaperView -->
|
||||
<vlc:VideoView x:Name="DesktopVideoWallpaperView"
|
||||
IsVisible="False"
|
||||
IsHitTestVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 主窗口代码变更 (MainWindow.axaml.cs)
|
||||
|
||||
#### 3.3.1 移除 using 声明
|
||||
|
||||
```csharp
|
||||
// 移除以下 using(如果存在)
|
||||
using LibVLCSharp.Shared;
|
||||
using LibVLCSharp.Avalonia;
|
||||
```
|
||||
|
||||
#### 3.3.2 移除静态字段
|
||||
|
||||
```csharp
|
||||
// 移除以下字段(约第68-71行)
|
||||
private static readonly HashSet<string> SupportedVideoExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v"
|
||||
};
|
||||
```
|
||||
|
||||
#### 3.3.3 移除实例字段
|
||||
|
||||
```csharp
|
||||
// 移除以下字段(约第123-146行)
|
||||
private Bitmap? _videoWallpaperPosterBitmap;
|
||||
private string? _videoWallpaperPosterPath;
|
||||
private string? _wallpaperVideoPath;
|
||||
private LibVLC? _libVlc;
|
||||
private MediaPlayer? _videoWallpaperPlayer;
|
||||
private Media? _videoWallpaperMedia;
|
||||
private readonly object _desktopVideoFrameSync = new();
|
||||
private MediaPlayer.LibVLCVideoLockCb? _desktopVideoLockCallback;
|
||||
private MediaPlayer.LibVLCVideoUnlockCb? _desktopVideoUnlockCallback;
|
||||
private MediaPlayer.LibVLCVideoDisplayCb? _desktopVideoDisplayCallback;
|
||||
private DispatcherTimer? _desktopVideoFrameRefreshTimer;
|
||||
private IntPtr _desktopVideoFrameBufferPtr;
|
||||
private byte[]? _desktopVideoStagingBuffer;
|
||||
private WriteableBitmap? _desktopVideoBitmap;
|
||||
private int _desktopVideoFrameWidth;
|
||||
private int _desktopVideoFrameHeight;
|
||||
private int _desktopVideoFramePitch;
|
||||
private int _desktopVideoFrameBufferSize;
|
||||
private int _desktopVideoFrameDirtyFlag;
|
||||
```
|
||||
|
||||
#### 3.3.4 修改 OnClosed 方法
|
||||
|
||||
移除视频相关清理代码(约第336-350行):
|
||||
|
||||
```csharp
|
||||
// 移除以下代码行
|
||||
StopVideoWallpaper();
|
||||
_videoWallpaperMedia?.Dispose();
|
||||
_videoWallpaperMedia = null;
|
||||
_videoWallpaperPlayer?.Dispose();
|
||||
_videoWallpaperPlayer = null;
|
||||
_desktopVideoFrameRefreshTimer?.Stop();
|
||||
_desktopVideoFrameRefreshTimer = null;
|
||||
_videoWallpaperPosterBitmap?.Dispose();
|
||||
_videoWallpaperPosterBitmap = null;
|
||||
_videoWallpaperPosterPath = null;
|
||||
_libVlc?.Dispose();
|
||||
_libVlc = null;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 主窗口 Stub 方法变更 (MainWindow.SettingsHardCut.Stubs.cs)
|
||||
|
||||
#### 3.4.1 移除 using 声明
|
||||
|
||||
```csharp
|
||||
// 移除以下 using(第19-20行)
|
||||
using LibVLCSharp.Shared;
|
||||
using LibVLCSharp.Avalonia;
|
||||
```
|
||||
|
||||
#### 3.4.2 移除方法
|
||||
|
||||
移除以下完整方法:
|
||||
|
||||
| 方法名 | 行号范围 | 说明 |
|
||||
|--------|----------|------|
|
||||
| `StartVideoWallpaper` | 337-383 | 启动视频壁纸播放 |
|
||||
| `StopVideoWallpaper` | 385-395 | 停止视频壁纸播放 |
|
||||
| `TryCaptureVideoWallpaperPosterFrame` | 666-751 | 捕获视频海报帧 |
|
||||
| `ApplyVideoWallpaperPosterVisibility` | 647-664 | 控制视频海报可见性 |
|
||||
|
||||
#### 3.4.3 修改 UpdateWallpaperDisplay 方法
|
||||
|
||||
简化为仅处理图片壁纸:
|
||||
|
||||
```csharp
|
||||
private void UpdateWallpaperDisplay()
|
||||
{
|
||||
// 移除视频分支,仅保留图片处理
|
||||
StopVideoWallpaper(); // 移除此调用
|
||||
ApplyWallpaperBrush();
|
||||
}
|
||||
```
|
||||
|
||||
修改后:
|
||||
|
||||
```csharp
|
||||
private void UpdateWallpaperDisplay()
|
||||
{
|
||||
ApplyWallpaperBrush();
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.4.4 修改 ApplyWallpaperBrush 方法
|
||||
|
||||
移除所有 `ApplyVideoWallpaperPosterVisibility` 调用:
|
||||
|
||||
```csharp
|
||||
// 移除以下调用
|
||||
ApplyVideoWallpaperPosterVisibility(showPoster: false);
|
||||
ApplyVideoWallpaperPosterVisibility(showPoster: _videoWallpaperPosterBitmap is not null);
|
||||
```
|
||||
|
||||
#### 3.4.5 修改 SetWallpaperState 方法
|
||||
|
||||
移除视频类型处理分支(约第238-247行):
|
||||
|
||||
```csharp
|
||||
// 移除以下代码块
|
||||
var requestedTypeIsVideo = string.Equals(_wallpaperType, "Video", StringComparison.OrdinalIgnoreCase);
|
||||
if (SupportedVideoExtensions.Contains(extension) || requestedTypeIsVideo)
|
||||
{
|
||||
_wallpaperMediaType = WallpaperMediaType.Video;
|
||||
_wallpaperVideoPath = _wallpaperPath;
|
||||
_wallpaperDisplayState = File.Exists(_wallpaperPath)
|
||||
? WallpaperDisplayState.CurrentValidWallpaper
|
||||
: WallpaperDisplayState.TemporarilyUnavailable;
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.5 外观主题服务变更 (AppearanceThemeService.cs)
|
||||
|
||||
#### 3.5.1 移除接口和类
|
||||
|
||||
移除以下代码(约第92-184行):
|
||||
|
||||
```csharp
|
||||
// 移除接口
|
||||
internal interface IVideoWallpaperSeedExtractor
|
||||
{
|
||||
IReadOnlyList<Color> ExtractSeedCandidates(string videoPath, MonetColorService monetColorService);
|
||||
}
|
||||
|
||||
// 移除实现类
|
||||
internal sealed class LibVlcVideoWallpaperSeedExtractor : IVideoWallpaperSeedExtractor
|
||||
{
|
||||
// ... 整个类实现
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.6 壁纸设置页面 XAML 变更 (WallpaperSettingsPage.axaml)
|
||||
|
||||
#### 3.6.1 移除视频预览区域
|
||||
|
||||
移除以下代码(约第29-44行):
|
||||
|
||||
```xml
|
||||
<Border Background="#FFF6F7F9"
|
||||
IsVisible="{Binding IsVideo}">
|
||||
<StackPanel HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="12">
|
||||
<fi:FluentIcon Icon="Video"
|
||||
Width="72"
|
||||
Height="72"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
<TextBlock Text="{Binding VideoModeHintText}"
|
||||
Width="300"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
```
|
||||
|
||||
#### 3.6.2 移除视频模式提示文本
|
||||
|
||||
移除以下代码(约第150-154行):
|
||||
|
||||
```xml
|
||||
<TextBlock Margin="0,8,0,0"
|
||||
IsVisible="{Binding IsVideo}"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="{Binding VideoModeHintText}"
|
||||
TextWrapping="Wrap" />
|
||||
```
|
||||
|
||||
#### 3.6.3 修改填充方式设置可见性绑定
|
||||
|
||||
```xml
|
||||
<!-- 修改前 -->
|
||||
IsVisible="{Binding IsImageOrVideo}"
|
||||
|
||||
<!-- 修改后 -->
|
||||
IsVisible="{Binding IsImage}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.7 壁纸设置 ViewModel 变更 (WallpaperSettingsPageViewModel.cs)
|
||||
|
||||
#### 3.7.1 移除属性
|
||||
|
||||
```csharp
|
||||
// 移除以下属性
|
||||
[ObservableProperty]
|
||||
private bool _isImageOrVideo;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isVideo;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _videoModeHintText = string.Empty;
|
||||
```
|
||||
|
||||
#### 3.7.2 修改 CreateWallpaperTypes 方法
|
||||
|
||||
```csharp
|
||||
// 修改前
|
||||
private IReadOnlyList<SelectionOption> CreateWallpaperTypes()
|
||||
{
|
||||
return
|
||||
[
|
||||
new SelectionOption("Image", L("settings.wallpaper.type.image", "Image")),
|
||||
new SelectionOption("Video", L("settings.wallpaper.type.video", "Video")),
|
||||
new SelectionOption("SolidColor", L("settings.wallpaper.type.solid_color", "Solid Color"))
|
||||
];
|
||||
}
|
||||
|
||||
// 修改后
|
||||
private IReadOnlyList<SelectionOption> CreateWallpaperTypes()
|
||||
{
|
||||
return
|
||||
[
|
||||
new SelectionOption("Image", L("settings.wallpaper.type.image", "Image")),
|
||||
new SelectionOption("SolidColor", L("settings.wallpaper.type.solid_color", "Solid Color"))
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.7.3 修改 UpdateVisibility 方法
|
||||
|
||||
移除 IsVideo 和 IsImageOrVideo 的赋值:
|
||||
|
||||
```csharp
|
||||
// 移除以下行
|
||||
IsVideo = SelectedWallpaperType?.Value == "Video";
|
||||
IsImageOrVideo = SelectedWallpaperType?.Value is "Image" or "Video";
|
||||
```
|
||||
|
||||
#### 3.7.4 修改 RefreshLocalizedText 方法
|
||||
|
||||
```csharp
|
||||
// 移除以下行
|
||||
VideoModeHintText = L("settings.wallpaper.video_mode", "Video wallpaper uses automatic fill mode.");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.8 设置契约变更 (SettingsContracts.cs)
|
||||
|
||||
#### 3.8.1 修改 WallpaperMediaType 枚举
|
||||
|
||||
```csharp
|
||||
// 修改前
|
||||
public enum WallpaperMediaType
|
||||
{
|
||||
None,
|
||||
Image,
|
||||
Video
|
||||
}
|
||||
|
||||
// 修改后
|
||||
public enum WallpaperMediaType
|
||||
{
|
||||
None,
|
||||
Image
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.9 设置域服务变更 (SettingsDomainServices.cs)
|
||||
|
||||
#### 3.9.1 移除视频扩展名集合
|
||||
|
||||
```csharp
|
||||
// 移除以下字段(约第150-153行)
|
||||
private static readonly HashSet<string> VideoExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v"
|
||||
};
|
||||
```
|
||||
|
||||
#### 3.9.2 修改 DetectMediaType 方法
|
||||
|
||||
```csharp
|
||||
// 修改前
|
||||
public WallpaperMediaType DetectMediaType(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return WallpaperMediaType.None;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path.Trim());
|
||||
if (string.IsNullOrWhiteSpace(extension))
|
||||
{
|
||||
return WallpaperMediaType.None;
|
||||
}
|
||||
|
||||
if (ImageExtensions.Contains(extension))
|
||||
{
|
||||
return WallpaperMediaType.Image;
|
||||
}
|
||||
|
||||
if (VideoExtensions.Contains(extension))
|
||||
{
|
||||
return WallpaperMediaType.Video;
|
||||
}
|
||||
|
||||
return WallpaperMediaType.None;
|
||||
}
|
||||
|
||||
// 修改后
|
||||
public WallpaperMediaType DetectMediaType(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return WallpaperMediaType.None;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path.Trim());
|
||||
if (string.IsNullOrWhiteSpace(extension))
|
||||
{
|
||||
return WallpaperMediaType.None;
|
||||
}
|
||||
|
||||
if (ImageExtensions.Contains(extension))
|
||||
{
|
||||
return WallpaperMediaType.Image;
|
||||
}
|
||||
|
||||
return WallpaperMediaType.None;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.10 本地化文件变更 (zh-CN.json)
|
||||
|
||||
#### 3.10.1 移除的本地化键
|
||||
|
||||
```json
|
||||
// 移除以下键值对
|
||||
"settings.wallpaper.type.video": "视频",
|
||||
"settings.wallpaper.video_applied": "视频壁纸已应用。",
|
||||
"settings.wallpaper.video_mode": "视频壁纸使用自动填充模式。",
|
||||
"settings.wallpaper.video_restored": "已恢复保存的视频壁纸。",
|
||||
"settings.wallpaper.video_not_found": "未找到视频壁纸文件。",
|
||||
"settings.wallpaper.video_player_unavailable": "视频播放器不可用。",
|
||||
"settings.wallpaper.video_play_failed_format": "播放视频壁纸失败:{0}"
|
||||
```
|
||||
|
||||
#### 3.10.2 修改描述文本
|
||||
|
||||
```json
|
||||
// 修改前
|
||||
"settings.wallpaper.description": "选择图片或视频后可立即设为应用窗口壁纸。",
|
||||
|
||||
// 修改后
|
||||
"settings.wallpaper.description": "选择图片后可立即设为应用窗口壁纸。",
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据模型变更
|
||||
|
||||
### 4.1 WallpaperMediaType 枚举简化
|
||||
|
||||
```
|
||||
变更前: None | Image | Video
|
||||
变更后: None | Image
|
||||
```
|
||||
|
||||
### 4.2 设置存储兼容性
|
||||
|
||||
现有用户设置中如果包含 `Type: "Video"` 的壁纸配置:
|
||||
- 应用将无法识别该类型
|
||||
- 将回退到纯色背景
|
||||
- 用户需要重新选择图片壁纸
|
||||
|
||||
---
|
||||
|
||||
## 5. 风险评估
|
||||
|
||||
### 5.1 潜在风险
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| 现有视频壁纸用户设置失效 | 中 | 应用会自动回退到纯色背景 |
|
||||
| 遗漏的视频相关代码引用 | 低 | 编译器会报告未定义类型错误 |
|
||||
| 本地化键遗漏 | 低 | 运行时会显示键名而非翻译文本 |
|
||||
|
||||
### 5.2 回滚策略
|
||||
|
||||
如需回滚,可通过 Git 恢复以下文件:
|
||||
- LanMountainDesktop.csproj
|
||||
- MainWindow.axaml / .axaml.cs
|
||||
- MainWindow.SettingsHardCut.Stubs.cs
|
||||
- AppearanceThemeService.cs
|
||||
- WallpaperSettingsPage.axaml
|
||||
- WallpaperSettingsPageViewModel.cs
|
||||
- SettingsContracts.cs
|
||||
- SettingsDomainServices.cs
|
||||
- zh-CN.json
|
||||
|
||||
---
|
||||
|
||||
## 6. 验证清单
|
||||
|
||||
### 6.1 编译验证
|
||||
|
||||
- [ ] 项目编译无错误
|
||||
- [ ] 无 LibVLC 相关类型引用警告
|
||||
- [ ] 无未使用变量警告
|
||||
|
||||
### 6.2 功能验证
|
||||
|
||||
- [ ] 应用正常启动
|
||||
- [ ] 图片壁纸正常显示
|
||||
- [ ] 纯色壁纸正常显示
|
||||
- [ ] 壁纸设置页面正常打开
|
||||
- [ ] 类型选择器仅显示"图片"和"纯色"
|
||||
- [ ] 壁纸导入功能正常工作
|
||||
|
||||
### 6.3 清理验证
|
||||
|
||||
- [ ] 无 LibVLC 相关 DLL 在输出目录
|
||||
- [ ] 无视频相关本地化文本残留
|
||||
- [ ] 无视频相关 UI 控件残留
|
||||
@@ -0,0 +1,206 @@
|
||||
# 移除视频壁纸功能规格说明书
|
||||
|
||||
## Why
|
||||
|
||||
当前 LanMountainDesktop 项目包含视频壁纸功能,该功能引入了以下复杂性和依赖:
|
||||
1. 引入了 LibVLCSharp.Avalonia、VideoLAN.LibVLC.Windows、VideoLAN.LibVLC.Mac 等重型依赖
|
||||
2. 在主窗口中残留大量视频壁纸相关代码和字段
|
||||
3. 在设置页面中保留了视频类型选择器和相关 UI 元素
|
||||
4. 在本地化文件中保留了视频壁纸相关文本
|
||||
5. 增加了应用复杂度和维护成本
|
||||
|
||||
用户决定移除该功能以简化代码库。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 移除 LibVLCSharp.Avalonia 及 VideoLAN.LibVLC.* NuGet 依赖
|
||||
- 移除 AppearanceThemeService.cs 中的 LibVlcVideoWallpaperSeedExtractor 类和 IVideoWallpaperSeedExtractor 接口
|
||||
- 移除 MainWindow.axaml.cs 中的视频壁纸相关字段和清理代码
|
||||
- 移除 MainWindow.SettingsHardCut.Stubs.cs 中的视频壁纸相关方法
|
||||
- 移除 MainWindow.axaml 中的 DesktopVideoWallpaperImage 和 DesktopVideoWallpaperView 控件
|
||||
- 移除 WallpaperSettingsPage.axaml 中的视频类型选择器和视频模式提示
|
||||
- 移除 WallpaperSettingsPageViewModel.cs 中的 IsVideo、VideoModeHintText 等属性
|
||||
- 移除 SettingsContracts.cs 中 WallpaperMediaType 枚举的 Video 值
|
||||
- 移除 SettingsDomainServices.cs 中 WallpaperMediaService 类的视频扩展名检测逻辑
|
||||
- 移除本地化文件中的视频壁纸相关文本
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected specs
|
||||
- 壁纸设置功能规格
|
||||
- 主窗口桌面层规格
|
||||
|
||||
### Affected code
|
||||
- `LanMountainDesktop.csproj` - NuGet 依赖配置
|
||||
- `Services/AppearanceThemeService.cs` - 视频壁纸种子提取器
|
||||
- `Views/MainWindow.axaml.cs` - 主窗口字段和清理逻辑
|
||||
- `Views/MainWindow.SettingsHardCut.Stubs.cs` - 视频壁纸控制方法
|
||||
- `Views/MainWindow.axaml` - 视频壁纸 UI 控件
|
||||
- `Views/SettingsPages/WallpaperSettingsPage.axaml` - 壁纸设置页面 UI
|
||||
- `ViewModels/WallpaperSettingsPageViewModel.cs` - 壁纸设置 ViewModel
|
||||
- `Services/Settings/SettingsContracts.cs` - 壁纸媒体类型枚举
|
||||
- `Services/Settings/SettingsDomainServices.cs` - 壁纸媒体服务
|
||||
- `Localization/zh-CN.json` - 本地化文本
|
||||
|
||||
---
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: 视频壁纸播放功能
|
||||
|
||||
**Reason**: 用户决定移除视频壁纸功能以简化代码库,减少重型依赖
|
||||
|
||||
**Migration**:
|
||||
- 用户如需动态壁纸,可使用静态图片壁纸替代
|
||||
- 现有视频壁纸设置将被重置为纯色背景
|
||||
|
||||
#### Scenario: 视频壁纸播放
|
||||
- **GIVEN** 用户选择了视频文件作为壁纸
|
||||
- **WHEN** 系统检测到视频格式
|
||||
- **THEN** 系统不再支持视频壁纸播放
|
||||
- **AND THEN** 系统提示用户该文件类型不受支持
|
||||
|
||||
### Requirement: LibVLC 依赖
|
||||
|
||||
**Reason**: 移除视频壁纸功能后不再需要 LibVLC 库
|
||||
|
||||
**Migration**: 从项目依赖中移除以下包:
|
||||
- LibVLCSharp.Avalonia
|
||||
- VideoLAN.LibVLC.Windows
|
||||
- VideoLAN.LibVLC.Mac
|
||||
|
||||
### Requirement: 视频壁纸种子提取
|
||||
|
||||
**Reason**: 移除视频壁纸功能后不再需要从视频中提取颜色种子
|
||||
|
||||
**Migration**: 移除 `LibVlcVideoWallpaperSeedExtractor` 类和 `IVideoWallpaperSeedExtractor` 接口
|
||||
|
||||
### Requirement: 视频壁纸 UI 控件
|
||||
|
||||
**Reason**: 移除视频壁纸功能后不再需要视频显示控件
|
||||
|
||||
**Migration**: 移除 `DesktopVideoWallpaperImage` 和 `DesktopVideoWallpaperView` 控件
|
||||
|
||||
### Requirement: 视频类型选择器
|
||||
|
||||
**Reason**: 移除视频壁纸功能后不再需要视频类型选项
|
||||
|
||||
**Migration**: 从壁纸类型选择器中移除"视频"选项
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 壁纸媒体类型检测
|
||||
|
||||
**当前**: 支持检测 None、Image、Video 三种类型
|
||||
|
||||
**修改后**: 仅支持检测 None、Image 两种类型
|
||||
|
||||
#### Scenario: 检测媒体类型
|
||||
- **WHEN** 用户选择壁纸文件
|
||||
- **THEN** 系统仅检测图片格式(.png, .jpg, .jpeg, .bmp, .gif, .webp)
|
||||
- **AND THEN** 视频格式文件将被识别为不受支持的类型
|
||||
|
||||
### Requirement: 壁纸类型选项
|
||||
|
||||
**当前**: 提供图片、视频、纯色三种类型选项
|
||||
|
||||
**修改后**: 仅提供图片、纯色两种类型选项
|
||||
|
||||
#### Scenario: 壁纸类型选择
|
||||
- **WHEN** 用户打开壁纸设置页面
|
||||
- **THEN** 类型选择器仅显示"图片"和"纯色"选项
|
||||
- **AND THEN** "视频"选项不再显示
|
||||
|
||||
### Requirement: 壁纸设置页面预览
|
||||
|
||||
**当前**: 根据类型显示图片预览、视频预览或纯色预览
|
||||
|
||||
**修改后**: 根据类型显示图片预览或纯色预览
|
||||
|
||||
#### Scenario: 预览显示
|
||||
- **WHEN** 用户选择壁纸类型
|
||||
- **THEN** 系统仅显示图片预览或纯色预览
|
||||
- **AND THEN** 视频预览区域不再显示
|
||||
|
||||
### Requirement: 主窗口壁纸显示
|
||||
|
||||
**当前**: 支持显示静态图片壁纸和视频壁纸
|
||||
|
||||
**修改后**: 仅支持显示静态图片壁纸
|
||||
|
||||
#### Scenario: 壁纸显示更新
|
||||
- **WHEN** 用户应用新壁纸
|
||||
- **THEN** 系统仅处理静态图片壁纸显示
|
||||
- **AND THEN** 视频壁纸播放逻辑不再执行
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 清理残留代码
|
||||
|
||||
系统 SHALL 完全移除视频壁纸功能相关的所有代码和资源。
|
||||
|
||||
#### Scenario: 主窗口字段清理
|
||||
- **WHEN** 执行代码清理
|
||||
- **THEN** 移除以下字段:
|
||||
- `_videoWallpaperPosterBitmap`
|
||||
- `_videoWallpaperPosterPath`
|
||||
- `_libVlc`
|
||||
- `_videoWallpaperPlayer`
|
||||
- `_videoWallpaperMedia`
|
||||
- `_wallpaperVideoPath`
|
||||
|
||||
#### Scenario: 主窗口方法清理
|
||||
- **WHEN** 执行代码清理
|
||||
- **THEN** 移除以下方法:
|
||||
- `StartVideoWallpaper`
|
||||
- `StopVideoWallpaper`
|
||||
- `TryCaptureVideoWallpaperPosterFrame`
|
||||
- `ApplyVideoWallpaperPosterVisibility`
|
||||
- `UpdateWallpaperDisplay` 中的视频处理分支
|
||||
|
||||
#### Scenario: ViewModel 属性清理
|
||||
- **WHEN** 执行代码清理
|
||||
- **THEN** 移除以下属性:
|
||||
- `IsVideo`
|
||||
- `VideoModeHintText`
|
||||
- `IsImageOrVideo`(改为 `IsImage`)
|
||||
|
||||
#### Scenario: 本地化文本清理
|
||||
- **WHEN** 执行代码清理
|
||||
- **THEN** 移除以下本地化键:
|
||||
- `settings.wallpaper.type.video`
|
||||
- `settings.wallpaper.video_applied`
|
||||
- `settings.wallpaper.video_mode`
|
||||
- `settings.wallpaper.video_restored`
|
||||
- `settings.wallpaper.video_not_found`
|
||||
- `settings.wallpaper.video_player_unavailable`
|
||||
- `settings.wallpaper.video_play_failed_format`
|
||||
|
||||
### Requirement: 依赖项清理
|
||||
|
||||
系统 SHALL 从项目文件中移除 LibVLC 相关 NuGet 包引用。
|
||||
|
||||
#### Scenario: NuGet 包移除
|
||||
- **WHEN** 执行依赖清理
|
||||
- **THEN** 移除以下包引用:
|
||||
- `LibVLCSharp.Avalonia`
|
||||
- `VideoLAN.LibVLC.Windows`
|
||||
- `VideoLAN.LibVLC.Mac`
|
||||
|
||||
### Requirement: 构建验证
|
||||
|
||||
系统 SHALL 在移除视频壁纸功能后保持正常构建和运行。
|
||||
|
||||
#### Scenario: 构建成功
|
||||
- **WHEN** 执行项目构建
|
||||
- **THEN** 构建成功无错误
|
||||
- **AND THEN** 所有现有测试通过
|
||||
|
||||
#### Scenario: 应用启动
|
||||
- **WHEN** 启动应用程序
|
||||
- **THEN** 应用正常启动
|
||||
- **AND THEN** 壁纸设置功能正常工作(仅支持图片和纯色)
|
||||
@@ -0,0 +1,600 @@
|
||||
# 移除视频壁纸功能 - 编码任务清单
|
||||
|
||||
## 任务概览
|
||||
|
||||
本文档将技术设计分解为可执行的编码任务,按依赖关系排序执行。
|
||||
|
||||
---
|
||||
|
||||
## 任务 1: 移除项目依赖
|
||||
|
||||
**优先级**: P0 (最高)
|
||||
**依赖**: 无
|
||||
**预估工作量**: 5 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
从项目文件中移除 LibVLC 相关的 NuGet 包引用。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/LanMountainDesktop.csproj`
|
||||
|
||||
### 输出
|
||||
|
||||
- 修改后的 `LanMountainDesktop.csproj`,移除以下包引用:
|
||||
- `LibVLCSharp.Avalonia`
|
||||
- `VideoLAN.LibVLC.Windows`
|
||||
- `VideoLAN.LibVLC.Mac`
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] 项目文件中不再包含 LibVLC 相关包引用
|
||||
- [ ] 执行 `dotnet restore` 成功
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 LanMountainDesktop.csproj,移除以下 PackageReference 节点:
|
||||
1. <PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" />
|
||||
2. <PackageReference Include="VideoLAN.LibVLC.Windows" ... />
|
||||
3. <PackageReference Include="VideoLAN.LibVLC.Mac" ... />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 2: 移除主窗口 XAML 视频控件
|
||||
|
||||
**优先级**: P0
|
||||
**依赖**: 任务 1
|
||||
**预估工作量**: 10 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
从 MainWindow.axaml 中移除视频壁纸相关的 XAML 控件和命名空间声明。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/Views/MainWindow.axaml`
|
||||
|
||||
### 输出
|
||||
|
||||
- 移除 LibVLC 命名空间声明
|
||||
- 移除 `DesktopVideoWallpaperImage` 控件
|
||||
- 移除 `DesktopVideoWallpaperView` 控件
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] XAML 中无 `xmlns:vlc` 命名空间
|
||||
- [ ] XAML 中无 `DesktopVideoWallpaperImage` 元素
|
||||
- [ ] XAML 中无 `DesktopVideoWallpaperView` 元素
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 MainWindow.axaml:
|
||||
1. 移除第 9 行: xmlns:vlc="clr-namespace:LibVLCSharp.Avalonia;assembly=LibVLCSharp.Avalonia"
|
||||
2. 移除第 126-131 行: <Image x:Name="DesktopVideoWallpaperImage" ... />
|
||||
3. 移除第 133-137 行: <vlc:VideoView x:Name="DesktopVideoWallpaperView" ... />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 3: 移除主窗口代码视频字段
|
||||
|
||||
**优先级**: P0
|
||||
**依赖**: 任务 1
|
||||
**预估工作量**: 15 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
从 MainWindow.axaml.cs 中移除视频壁纸相关的字段声明。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/Views/MainWindow.axaml.cs`
|
||||
|
||||
### 输出
|
||||
|
||||
- 移除 `SupportedVideoExtensions` 静态字段
|
||||
- 移除所有视频相关实例字段
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] 无 `SupportedVideoExtensions` 字段
|
||||
- [ ] 无 `_videoWallpaperPosterBitmap` 字段
|
||||
- [ ] 无 `_videoWallpaperPosterPath` 字段
|
||||
- [ ] 无 `_wallpaperVideoPath` 字段
|
||||
- [ ] 无 `_libVlc` 字段
|
||||
- [ ] 无 `_videoWallpaperPlayer` 字段
|
||||
- [ ] 无 `_videoWallpaperMedia` 字段
|
||||
- [ ] 无 `_desktopVideoFrameSync` 及相关视频帧处理字段
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 MainWindow.axaml.cs:
|
||||
1. 移除第 68-71 行的 SupportedVideoExtensions 定义
|
||||
2. 移除第 123-146 行的所有视频相关字段
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 4: 移除主窗口 OnClosed 清理代码
|
||||
|
||||
**优先级**: P0
|
||||
**依赖**: 任务 3
|
||||
**预估工作量**: 5 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
从 MainWindow.axaml.cs 的 OnClosed 方法中移除视频相关清理代码。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/Views/MainWindow.axaml.cs` (OnClosed 方法)
|
||||
|
||||
### 输出
|
||||
|
||||
- 简化的 OnClosed 方法,无视频清理逻辑
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] OnClosed 方法中无 `StopVideoWallpaper()` 调用
|
||||
- [ ] OnClosed 方法中无 `_videoWallpaperMedia` 相关清理
|
||||
- [ ] OnClosed 方法中无 `_videoWallpaperPlayer` 相关清理
|
||||
- [ ] OnClosed 方法中无 `_libVlc` 相关清理
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 MainWindow.axaml.cs 的 OnClosed 方法,移除以下代码行:
|
||||
- StopVideoWallpaper();
|
||||
- _videoWallpaperMedia?.Dispose(); _videoWallpaperMedia = null;
|
||||
- _videoWallpaperPlayer?.Dispose(); _videoWallpaperPlayer = null;
|
||||
- _desktopVideoFrameRefreshTimer?.Stop(); _desktopVideoFrameRefreshTimer = null;
|
||||
- _videoWallpaperPosterBitmap?.Dispose(); _videoWallpaperPosterBitmap = null;
|
||||
- _videoWallpaperPosterPath = null;
|
||||
- _libVlc?.Dispose(); _libVlc = null;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 5: 移除主窗口 Stub 方法
|
||||
|
||||
**优先级**: P0
|
||||
**依赖**: 任务 1
|
||||
**预估工作量**: 20 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
从 MainWindow.SettingsHardCut.Stubs.cs 中移除视频壁纸相关方法和 using 声明。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs`
|
||||
|
||||
### 输出
|
||||
|
||||
- 移除 LibVLC using 声明
|
||||
- 移除 `StartVideoWallpaper` 方法
|
||||
- 移除 `StopVideoWallpaper` 方法
|
||||
- 移除 `TryCaptureVideoWallpaperPosterFrame` 方法
|
||||
- 移除 `ApplyVideoWallpaperPosterVisibility` 方法
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] 无 `using LibVLCSharp.Shared;`
|
||||
- [ ] 无 `using LibVLCSharp.Avalonia;`
|
||||
- [ ] 无 `StartVideoWallpaper` 方法定义
|
||||
- [ ] 无 `StopVideoWallpaper` 方法定义
|
||||
- [ ] 无 `TryCaptureVideoWallpaperPosterFrame` 方法定义
|
||||
- [ ] 无 `ApplyVideoWallpaperPosterVisibility` 方法定义
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 MainWindow.SettingsHardCut.Stubs.cs:
|
||||
1. 移除第 19-20 行的 using 声明
|
||||
2. 移除 StartVideoWallpaper 方法(第 337-383 行)
|
||||
3. 移除 StopVideoWallpaper 方法(第 385-395 行)
|
||||
4. 移除 ApplyVideoWallpaperPosterVisibility 方法(第 647-664 行)
|
||||
5. 移除 TryCaptureVideoWallpaperPosterFrame 方法(第 666-751 行)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 6: 简化壁纸状态处理逻辑
|
||||
|
||||
**优先级**: P0
|
||||
**依赖**: 任务 5
|
||||
**预估工作量**: 15 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
修改 MainWindow.SettingsHardCut.Stubs.cs 中的壁纸状态处理方法,移除视频类型分支。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs`
|
||||
|
||||
### 输出
|
||||
|
||||
- 简化的 `SetWallpaperState` 方法
|
||||
- 简化的 `UpdateWallpaperDisplay` 方法
|
||||
- 简化的 `ApplyWallpaperBrush` 方法
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] `SetWallpaperState` 中无视频类型检测分支
|
||||
- [ ] `SetWallpaperState` 中无 `_wallpaperVideoPath` 赋值
|
||||
- [ ] `UpdateWallpaperDisplay` 中无 `StopVideoWallpaper()` 调用
|
||||
- [ ] `ApplyWallpaperBrush` 中无 `ApplyVideoWallpaperPosterVisibility` 调用
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 MainWindow.SettingsHardCut.Stubs.cs:
|
||||
|
||||
1. SetWallpaperState 方法:
|
||||
- 移除 requestedTypeIsVideo 变量定义
|
||||
- 移除视频类型检测 if 块(SupportedVideoExtensions.Contains 检查)
|
||||
|
||||
2. UpdateWallpaperDisplay 方法:
|
||||
- 移除视频类型分支,仅保留 ApplyWallpaperBrush() 调用
|
||||
|
||||
3. ApplyWallpaperBrush 方法:
|
||||
- 移除所有 ApplyVideoWallpaperPosterVisibility 调用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 7: 移除外观主题服务视频提取器
|
||||
|
||||
**优先级**: P1
|
||||
**依赖**: 任务 1
|
||||
**预估工作量**: 10 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
从 AppearanceThemeService.cs 中移除视频壁纸种子提取器接口和实现类。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/Services/AppearanceThemeService.cs`
|
||||
|
||||
### 输出
|
||||
|
||||
- 移除 `IVideoWallpaperSeedExtractor` 接口
|
||||
- 移除 `LibVlcVideoWallpaperSeedExtractor` 类
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] 无 `IVideoWallpaperSeedExtractor` 接口定义
|
||||
- [ ] 无 `LibVlcVideoWallpaperSeedExtractor` 类定义
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 AppearanceThemeService.cs:
|
||||
移除第 92-184 行的接口和类定义:
|
||||
- IVideoWallpaperSeedExtractor 接口
|
||||
- LibVlcVideoWallpaperSeedExtractor 类
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 8: 简化壁纸设置页面 XAML
|
||||
|
||||
**优先级**: P1
|
||||
**依赖**: 无
|
||||
**预估工作量**: 10 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
从 WallpaperSettingsPage.axaml 中移除视频预览区域和相关 UI 元素。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml`
|
||||
|
||||
### 输出
|
||||
|
||||
- 移除视频预览 Border 区域
|
||||
- 移除视频模式提示 TextBlock
|
||||
- 修改填充方式可见性绑定
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] 无视频预览 Border(IsVisible="{Binding IsVideo}")
|
||||
- [ ] 无 VideoModeHintText 绑定的 TextBlock
|
||||
- [ ] 填充方式设置绑定改为 `IsVisible="{Binding IsImage}"`
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 WallpaperSettingsPage.axaml:
|
||||
1. 移除第 29-44 行的视频预览 Border
|
||||
2. 移除第 150-154 行的视频模式提示 TextBlock
|
||||
3. 修改第 132 行: IsVisible="{Binding IsImageOrVideo}" 改为 IsVisible="{Binding IsImage}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 9: 简化壁纸设置 ViewModel
|
||||
|
||||
**优先级**: P1
|
||||
**依赖**: 任务 8
|
||||
**预估工作量**: 15 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
从 WallpaperSettingsPageViewModel.cs 中移除视频相关属性和方法逻辑。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs`
|
||||
|
||||
### 输出
|
||||
|
||||
- 移除 `_isImageOrVideo`、`_isVideo`、`_videoModeHintText` 属性
|
||||
- 修改 `CreateWallpaperTypes` 方法
|
||||
- 修改 `UpdateVisibility` 方法
|
||||
- 修改 `RefreshLocalizedText` 方法
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] 无 `IsImageOrVideo` 属性
|
||||
- [ ] 无 `IsVideo` 属性
|
||||
- [ ] 无 `VideoModeHintText` 属性
|
||||
- [ ] `CreateWallpaperTypes` 仅返回 Image 和 SolidColor 选项
|
||||
- [ ] `UpdateVisibility` 中无 IsVideo、IsImageOrVideo 赋值
|
||||
- [ ] `RefreshLocalizedText` 中无 VideoModeHintText 赋值
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 WallpaperSettingsPageViewModel.cs:
|
||||
1. 移除第 76-77 行的 _isImageOrVideo 字段和属性
|
||||
2. 移除第 85-86 行的 _isVideo 字段和属性
|
||||
3. 移除第 94-95 行的 _videoModeHintText 字段和属性
|
||||
4. 修改 CreateWallpaperTypes 方法,移除 Video 选项
|
||||
5. 修改 UpdateVisibility 方法,移除 IsVideo 和 IsImageOrVideo 赋值
|
||||
6. 修改 RefreshLocalizedText 方法,移除 VideoModeHintText 赋值
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 10: 简化壁纸媒体类型枚举
|
||||
|
||||
**优先级**: P1
|
||||
**依赖**: 无
|
||||
**预估工作量**: 5 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
从 SettingsContracts.cs 中移除 WallpaperMediaType 枚举的 Video 值。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/Services/Settings/SettingsContracts.cs`
|
||||
|
||||
### 输出
|
||||
|
||||
- 简化的 `WallpaperMediaType` 枚举
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] `WallpaperMediaType` 枚举仅包含 `None` 和 `Image`
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 SettingsContracts.cs:
|
||||
修改第 11-16 行的枚举定义:
|
||||
public enum WallpaperMediaType
|
||||
{
|
||||
None,
|
||||
Image
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 11: 简化壁纸媒体服务
|
||||
|
||||
**优先级**: P1
|
||||
**依赖**: 任务 10
|
||||
**预估工作量**: 10 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
从 SettingsDomainServices.cs 中移除视频扩展名检测逻辑。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/Services/Settings/SettingsDomainServices.cs`
|
||||
|
||||
### 输出
|
||||
|
||||
- 移除 `VideoExtensions` 字段
|
||||
- 简化 `DetectMediaType` 方法
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] 无 `VideoExtensions` 字段定义
|
||||
- [ ] `DetectMediaType` 方法中无视频扩展名检测逻辑
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 SettingsDomainServices.cs:
|
||||
1. 移除第 150-153 行的 VideoExtensions 字段定义
|
||||
2. 修改 DetectMediaType 方法,移除视频检测分支
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 12: 清理本地化文件
|
||||
|
||||
**优先级**: P2
|
||||
**依赖**: 无
|
||||
**预估工作量**: 5 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
从 zh-CN.json 中移除视频壁纸相关的本地化文本。
|
||||
|
||||
### 输入
|
||||
|
||||
- `LanMountainDesktop/Localization/zh-CN.json`
|
||||
|
||||
### 输出
|
||||
|
||||
- 移除视频相关本地化键
|
||||
- 修改壁纸描述文本
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] 无 `settings.wallpaper.type.video` 键
|
||||
- [ ] 无 `settings.wallpaper.video_applied` 键
|
||||
- [ ] 无 `settings.wallpaper.video_mode` 键
|
||||
- [ ] 无 `settings.wallpaper.video_restored` 键
|
||||
- [ ] 无 `settings.wallpaper.video_not_found` 键
|
||||
- [ ] 无 `settings.wallpaper.video_player_unavailable` 键
|
||||
- [ ] 无 `settings.wallpaper.video_play_failed_format` 键
|
||||
- [ ] `settings.wallpaper.description` 文本已更新
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
编辑 zh-CN.json:
|
||||
1. 移除以下键值对:
|
||||
- "settings.wallpaper.type.video"
|
||||
- "settings.wallpaper.video_applied"
|
||||
- "settings.wallpaper.video_mode"
|
||||
- "settings.wallpaper.video_restored"
|
||||
- "settings.wallpaper.video_not_found"
|
||||
- "settings.wallpaper.video_player_unavailable"
|
||||
- "settings.wallpaper.video_play_failed_format"
|
||||
|
||||
2. 修改描述文本:
|
||||
"settings.wallpaper.description": "选择图片后可立即设为应用窗口壁纸。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 13: 构建验证
|
||||
|
||||
**优先级**: P0
|
||||
**依赖**: 任务 1-12 全部完成
|
||||
**预估工作量**: 10 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
验证项目在移除视频壁纸功能后能够正常构建。
|
||||
|
||||
### 输入
|
||||
|
||||
- 整个项目
|
||||
|
||||
### 输出
|
||||
|
||||
- 构建成功确认
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] `dotnet build` 执行成功,无编译错误
|
||||
- [ ] 无 LibVLC 相关类型未定义错误
|
||||
- [ ] 无未使用变量警告(或已处理)
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
在项目根目录执行:
|
||||
dotnet build LanMountainDesktop/LanMountainDesktop.csproj
|
||||
|
||||
检查输出:
|
||||
- 确认无编译错误
|
||||
- 确认无 LibVLC 相关类型引用错误
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 14: 功能验证
|
||||
|
||||
**优先级**: P0
|
||||
**依赖**: 任务 13
|
||||
**预估工作量**: 15 分钟
|
||||
|
||||
### 描述
|
||||
|
||||
验证应用在移除视频壁纸功能后核心功能正常工作。
|
||||
|
||||
### 输入
|
||||
|
||||
- 构建后的应用
|
||||
|
||||
### 输出
|
||||
|
||||
- 功能验证报告
|
||||
|
||||
### 验收标准
|
||||
|
||||
- [ ] 应用正常启动
|
||||
- [ ] 图片壁纸正常显示
|
||||
- [ ] 纯色壁纸正常显示
|
||||
- [ ] 壁纸设置页面正常打开
|
||||
- [ ] 类型选择器仅显示"图片"和"纯色"选项
|
||||
- [ ] 壁纸导入功能正常工作
|
||||
|
||||
### 执行提示
|
||||
|
||||
```
|
||||
运行应用:
|
||||
dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj
|
||||
|
||||
手动验证:
|
||||
1. 应用启动无崩溃
|
||||
2. 打开设置 -> 壁纸页面
|
||||
3. 确认类型选择器仅有"图片"和"纯色"
|
||||
4. 测试选择图片壁纸
|
||||
5. 测试选择纯色壁纸
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务依赖关系图
|
||||
|
||||
```
|
||||
任务 1 (移除依赖)
|
||||
├── 任务 2 (XAML控件)
|
||||
├── 任务 3 (代码字段)
|
||||
│ └── 任务 4 (OnClosed清理)
|
||||
├── 任务 5 (Stub方法)
|
||||
│ └── 任务 6 (状态处理逻辑)
|
||||
└── 任务 7 (主题服务)
|
||||
|
||||
任务 8 (设置页面XAML)
|
||||
└── 任务 9 (设置ViewModel)
|
||||
|
||||
任务 10 (枚举简化)
|
||||
└── 任务 11 (媒体服务)
|
||||
|
||||
任务 12 (本地化) - 独立
|
||||
|
||||
任务 13 (构建验证) - 依赖所有任务
|
||||
└── 任务 14 (功能验证)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 执行顺序建议
|
||||
|
||||
按以下顺序执行可确保依赖关系正确:
|
||||
|
||||
1. **第一批** (可并行): 任务 1, 任务 8, 任务 10, 任务 12
|
||||
2. **第二批** (可并行): 任务 2, 任务 3, 任务 5, 任务 7, 任务 9, 任务 11
|
||||
3. **第三批** (可并行): 任务 4, 任务 6
|
||||
4. **第四批**: 任务 13
|
||||
5. **第五批**: 任务 14
|
||||
@@ -67,6 +67,15 @@ public partial class App : Application
|
||||
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
|
||||
(Current as App)?._hostApplicationLifecycle;
|
||||
|
||||
// 隐私政策查看事件
|
||||
public static event Action? CurrentPrivacyPolicyViewRequested;
|
||||
|
||||
// 触发隐私政策查看事件的方法
|
||||
public static void RaisePrivacyPolicyViewRequested()
|
||||
{
|
||||
CurrentPrivacyPolicyViewRequested?.Invoke();
|
||||
}
|
||||
|
||||
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
|
||||
public ISettingsFacadeService SettingsFacade => _settingsFacade;
|
||||
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
|
||||
@@ -501,6 +510,8 @@ public partial class App : Application
|
||||
|
||||
if (languageChanged)
|
||||
{
|
||||
// 清除本地化缓存,强制重新加载语言文件
|
||||
_localizationService.ClearCache();
|
||||
ApplyCurrentCultureFromSettings();
|
||||
if (_trayIcons is not null)
|
||||
{
|
||||
|
||||
326
LanMountainDesktop/Assets/Documents/Privacy.md
Normal file
326
LanMountainDesktop/Assets/Documents/Privacy.md
Normal file
@@ -0,0 +1,326 @@
|
||||
# LanMountainDesktop 隐私政策
|
||||
|
||||
**最后更新日期:2026年3月17日**
|
||||
|
||||
---
|
||||
|
||||
## 引言
|
||||
|
||||
欢迎使用 LanMountainDesktop!我们非常重视您的隐私保护。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的数据。
|
||||
|
||||
**请在使用本应用前仔细阅读本隐私政策。使用本应用即表示您同意本政策的条款。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 数据收集范围
|
||||
|
||||
### 1.1 我们收集的数据
|
||||
|
||||
当您启用匿名数据收集功能时,我们会收集以下数据:
|
||||
|
||||
#### 匿名崩溃数据
|
||||
- **崩溃报告**:应用崩溃时的错误日志和堆栈跟踪
|
||||
- **设备信息**:操作系统版本、设备型号、架构(x64/x86)
|
||||
- **应用版本**:当前使用的应用版本号
|
||||
- **设备标识符**:匿名生成的唯一设备ID(不包含个人信息)
|
||||
|
||||
#### 匿名使用数据
|
||||
- **应用启动和关闭事件**:记录应用何时启动和关闭
|
||||
- **功能使用统计**:哪些功能被使用、使用频率
|
||||
- **设置变更**:用户更改了哪些设置(不包含具体设置值)
|
||||
- **界面交互**:点击了哪些按钮、访问了哪些页面
|
||||
- **设备信息**:操作系统、应用版本、设备类型
|
||||
|
||||
### 1.2 始终收集的基础数据
|
||||
|
||||
**重要说明:** 为了统计应用的用户数量和日活跃用户,即使您关闭了匿名数据收集开关,我们仍会收集以下基础数据:
|
||||
|
||||
- ✅ **应用启动事件**:用于统计日活跃用户
|
||||
- ✅ **设备标识符**:用于区分不同用户(不包含个人信息)
|
||||
- ✅ **应用版本**:用于统计版本分布
|
||||
|
||||
**这些基础数据不包含任何个人身份信息,仅用于统计用户数量和应用使用情况。**
|
||||
|
||||
### 1.3 我们不收集的数据
|
||||
|
||||
我们**明确承诺不收集**以下数据:
|
||||
|
||||
- ❌ 个人身份信息(姓名、邮箱、电话等)
|
||||
- ❌ 真实姓名或用户名
|
||||
- ❌ 地理位置信息(精确位置)
|
||||
- ❌ 文件内容或文档数据
|
||||
- ❌ 密码或凭据信息
|
||||
- ❌ 网络浏览历史
|
||||
- ❌ 联系人信息
|
||||
- ❌ 照片、视频或音频文件
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据收集目的
|
||||
|
||||
我们收集数据的目的如下:
|
||||
|
||||
### 2.1 基础数据用途(始终收集)
|
||||
- **统计用户数量**:了解应用的用户规模
|
||||
- **统计日活跃用户**:了解应用的活跃程度
|
||||
- **版本分布统计**:了解用户使用的版本情况
|
||||
|
||||
### 2.2 崩溃数据用途
|
||||
- **提高应用稳定性**:识别和修复崩溃问题
|
||||
- **优化性能**:分析性能瓶颈
|
||||
- **改进用户体验**:了解应用在不同设备上的表现
|
||||
|
||||
### 2.3 使用数据用途
|
||||
- **功能优化**:了解哪些功能最受欢迎,优先改进
|
||||
- **用户体验改进**:优化界面设计和交互流程
|
||||
- **统计分析**:了解用户规模和使用趋势
|
||||
- **产品决策**:基于数据做出产品发展方向决策
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据存储和处理
|
||||
|
||||
### 3.1 数据存储位置
|
||||
|
||||
我们使用以下第三方服务存储和处理数据:
|
||||
|
||||
#### Sentry(崩溃报告)
|
||||
- **用途**:崩溃数据收集和分析
|
||||
- **位置**:美国
|
||||
- **官网**:https://sentry.io
|
||||
- **隐私政策**:https://sentry.io/privacy/
|
||||
|
||||
#### PostHog(使用分析)
|
||||
- **用途**:用户行为分析和统计
|
||||
- **位置**:美国
|
||||
- **官网**:https://posthog.com
|
||||
- **隐私政策**:https://posthog.com/privacy
|
||||
|
||||
### 3.2 数据保留期限
|
||||
|
||||
- **崩溃数据**:保留90天后自动删除
|
||||
- **使用数据**:保留12个月后自动删除
|
||||
- **设备标识符**:永久保留(用于统计日活用户)
|
||||
|
||||
### 3.3 数据安全措施
|
||||
|
||||
我们采取以下安全措施保护您的数据:
|
||||
|
||||
- ✅ 数据传输使用HTTPS加密
|
||||
- ✅ 数据存储使用加密技术
|
||||
- ✅ 访问权限严格控制
|
||||
- ✅ 定期安全审计
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据共享
|
||||
|
||||
### 4.1 我们不会出售您的数据
|
||||
|
||||
我们**明确承诺**:
|
||||
- ❌ 不会出售您的个人数据
|
||||
- ❌ 不会将您的数据用于广告目的
|
||||
- ❌ 不会与第三方共享可识别个人的数据
|
||||
|
||||
### 4.2 必要的共享
|
||||
|
||||
我们仅在以下情况下共享数据:
|
||||
|
||||
- **服务提供商**:与Sentry和PostHog共享数据以提供服务
|
||||
- **法律要求**:在法律要求或政府机构合法要求时
|
||||
|
||||
---
|
||||
|
||||
## 5. 您的权利
|
||||
|
||||
### 5.1 选择权
|
||||
|
||||
您完全控制详细数据收集:
|
||||
|
||||
- **匿名崩溃数据**:可在设置中开启或关闭
|
||||
- **匿名使用数据**:可在设置中开启或关闭
|
||||
- **基础数据**:始终收集(用于统计用户数量)
|
||||
|
||||
**注意:** 即使关闭所有开关,我们仍会收集基础数据(应用启动事件和设备标识符)以统计用户数量。
|
||||
|
||||
### 5.2 数据访问权
|
||||
|
||||
您可以:
|
||||
- 查看我们收集的数据类型
|
||||
- 了解数据的使用目的
|
||||
- 了解数据的存储位置
|
||||
|
||||
### 5.3 数据删除权
|
||||
|
||||
您可以:
|
||||
- 随时关闭详细数据收集功能
|
||||
- 删除本地存储的设备标识符
|
||||
- 联系我们删除已收集的数据
|
||||
|
||||
---
|
||||
|
||||
## 6. 设备标识符
|
||||
|
||||
### 6.1 什么是设备标识符?
|
||||
|
||||
设备标识符是一个随机生成的唯一字符串,用于:
|
||||
- 统计日活用户数量
|
||||
- 区分不同设备
|
||||
- 分析用户使用趋势
|
||||
|
||||
### 6.2 设备标识符的特点
|
||||
|
||||
- **匿名性**:不包含任何个人信息
|
||||
- **随机性**:通过SHA256算法生成
|
||||
- **唯一性**:每个设备有唯一标识符
|
||||
- **持久性**:即使刷新设备ID,仍能关联到同一用户
|
||||
- **可重置**:您可以在设置中刷新设备标识符
|
||||
|
||||
### 6.3 设备标识符刷新
|
||||
|
||||
当您刷新设备标识符时:
|
||||
- 生成新的设备ID
|
||||
- **持久用户ID保持不变**,确保仍能关联到同一用户
|
||||
- 统计数据不会丢失
|
||||
|
||||
### 6.4 设备标识符示例
|
||||
|
||||
```
|
||||
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 儿童隐私保护
|
||||
|
||||
本应用不面向13岁以下儿童。我们不会故意收集儿童的个人信息。如果您发现我们无意中收集了儿童的数据,请联系我们,我们将立即删除相关数据。
|
||||
|
||||
---
|
||||
|
||||
## 8. 国际数据传输
|
||||
|
||||
由于我们的服务提供商位于美国,您的数据可能会被传输到美国。我们确保:
|
||||
|
||||
- 数据传输符合相关法律法规
|
||||
- 服务提供商遵守GDPR等隐私法规
|
||||
- 采取适当的安全措施保护数据
|
||||
|
||||
---
|
||||
|
||||
## 9. 隐私政策更新
|
||||
|
||||
我们可能会不时更新本隐私政策。更新时,我们将:
|
||||
|
||||
- 在本页面更新"最后更新日期"
|
||||
- 在应用内通知您重大变更
|
||||
- 继续使用应用即表示您同意更新后的政策
|
||||
|
||||
---
|
||||
|
||||
## 10. 联系我们
|
||||
|
||||
如果您对本隐私政策有任何疑问或建议,请通过以下方式联系我们:
|
||||
|
||||
- **GitHub Issues**:https://github.com/wwiinnddyy/LanMountainDesktop/issues
|
||||
- **电子邮件**:[您的邮箱地址]
|
||||
|
||||
---
|
||||
|
||||
## 11. 法律依据
|
||||
|
||||
### 11.1 GDPR合规
|
||||
|
||||
如果您位于欧洲经济区(EEA),我们的数据处理基于:
|
||||
|
||||
- **同意**:您明确同意数据收集
|
||||
- **合法利益**:改进应用性能和用户体验
|
||||
|
||||
### 11.2 CCPA合规
|
||||
|
||||
如果您是加州居民,您有权:
|
||||
|
||||
- 知道我们收集了哪些数据
|
||||
- 要求删除您的数据
|
||||
- 选择退出数据销售(我们不销售数据)
|
||||
|
||||
---
|
||||
|
||||
## 12. 第三方链接
|
||||
|
||||
本应用可能包含第三方网站链接。我们不对这些网站的隐私政策负责。请阅读这些网站的隐私政策。
|
||||
|
||||
---
|
||||
|
||||
## 13. 数据收集示例
|
||||
|
||||
### 13.1 崩溃报告示例
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "abc123",
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"device_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
|
||||
"app_version": "1.0.0",
|
||||
"os_name": "Windows",
|
||||
"os_version": "10.0.19041",
|
||||
"error_message": "NullReferenceException",
|
||||
"stack_trace": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 13.2 使用数据示例
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "app_online",
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"distinct_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
|
||||
"properties": {
|
||||
"app_version": "1.0.0",
|
||||
"os_name": "Windows",
|
||||
"event_type": "app_start",
|
||||
"analytics_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 13.3 基础数据示例(始终收集)
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "$pageview",
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"distinct_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
|
||||
"properties": {
|
||||
"$current_url": "app://main",
|
||||
"$title": "LanMountainDesktop"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. 您的同意
|
||||
|
||||
使用本应用即表示您:
|
||||
|
||||
- ✅ 已阅读并理解本隐私政策
|
||||
- ✅ 同意我们按照本政策收集和使用数据
|
||||
- ✅ 了解您可以随时撤回同意(详细数据收集)
|
||||
- ✅ 了解基础数据将始终收集以统计用户数量
|
||||
|
||||
---
|
||||
|
||||
## 15. 免责声明
|
||||
|
||||
本隐私政策仅适用于 LanMountainDesktop 应用。我们不对以下情况负责:
|
||||
|
||||
- 第三方服务的隐私政策
|
||||
- 您自行分享的数据
|
||||
- 不可抗力导致的数据泄露
|
||||
|
||||
---
|
||||
|
||||
**感谢您信任阑山桌面LanMountainDesktop!**
|
||||
|
||||
我们承诺保护您的隐私,并持续改进我们的隐私保护措施。
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public static class BuiltInComponentIds
|
||||
{
|
||||
@@ -40,4 +40,5 @@ public static class BuiltInComponentIds
|
||||
public const string DesktopWhiteboard = "DesktopWhiteboard";
|
||||
public const string DesktopBlackboardLandscape = "DesktopBlackboardLandscape";
|
||||
public const string DesktopBrowser = "DesktopBrowser";
|
||||
public const string DesktopOfficeRecentDocuments = "DesktopOfficeRecentDocuments";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Views;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public static class ComponentColorSchemeHelper
|
||||
{
|
||||
public static bool ShouldUseMonetColor(string? componentColorScheme, string globalThemeColorMode)
|
||||
{
|
||||
if (string.Equals(componentColorScheme, ThemeAppearanceValues.ColorSchemeNative, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.Equals(componentColorScheme, ThemeAppearanceValues.ColorSchemeFollowSystem, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !string.Equals(globalThemeColorMode, ThemeAppearanceValues.ColorModeDefaultNeutral, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string GetCurrentGlobalThemeColorMode()
|
||||
{
|
||||
try
|
||||
{
|
||||
var service = HostAppearanceThemeProvider.GetOrCreate();
|
||||
var appearance = service.GetCurrent();
|
||||
return appearance?.ThemeColorMode ?? ThemeAppearanceValues.ColorModeDefaultNeutral;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ThemeAppearanceValues.ColorModeDefaultNeutral;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using LanMountainDesktop.ComponentSystem.Extensions;
|
||||
@@ -327,6 +327,15 @@ public sealed class ComponentRegistry
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true,
|
||||
ResizeMode: DesktopComponentResizeMode.Free),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopOfficeRecentDocuments,
|
||||
"Office Recent Documents",
|
||||
"Folder",
|
||||
"File",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.Date,
|
||||
"Calendar",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
@@ -21,6 +21,9 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Models\" />
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
<AvaloniaResource Include="Localization\**" />
|
||||
<EmbeddedResource Include="Assets\Documents\Privacy.md" />
|
||||
<EmbeddedResource Include="Localization\*.json" />
|
||||
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="Extensions\Components\*.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
@@ -52,15 +55,14 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
|
||||
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" />
|
||||
|
||||
<PackageReference Include="PortAudioSharp2" Version="1.0.6" />
|
||||
<PackageReference Include="MaterialColorUtilities" Version="0.3.0" />
|
||||
<PackageReference Include="PostHog" Version="2.4.0" />
|
||||
<PackageReference Include="Sentry" Version="4.0.0" />
|
||||
<PackageReference Include="System.Runtime.WindowsRuntime" Version="4.7.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
|
||||
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" Condition="('$(RuntimeIdentifier)' == '' and $([MSBuild]::IsOSPlatform('Windows')))
 or '$(RuntimeIdentifier)' == 'win-x64'
 or '$(RuntimeIdentifier)' == 'win-x86'" />
|
||||
<PackageReference Include="VideoLAN.LibVLC.Mac" Version="3.1.3.1" Condition="('$(RuntimeIdentifier)' == '' and $([MSBuild]::IsOSPlatform('OSX')))
 or '$(RuntimeIdentifier)' == 'osx-x64'" />
|
||||
|
||||
<PackageReference Include="WebView.Avalonia" Version="11.0.0.1" />
|
||||
<PackageReference Include="WebView.Avalonia.Desktop" Version="11.0.0.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
|
||||
@@ -99,6 +99,11 @@
|
||||
"settings.privacy.crash_upload_description": "Help us improve application stability.",
|
||||
"settings.privacy.usage_upload_title": "Anonymous usage data uploads",
|
||||
"settings.privacy.usage_upload_description": "Help us improve application features.",
|
||||
"settings.privacy.device_id_title": "Device ID",
|
||||
"settings.privacy.device_id_description": "Unique identifier for this device. Click refresh to regenerate.",
|
||||
"settings.privacy.refresh_device_id": "Refresh",
|
||||
"settings.privacy.policy_hint_prefix": "For more details, please ",
|
||||
"settings.privacy.view_policy": "view our privacy policy",
|
||||
"settings.weather.title": "Weather",
|
||||
"settings.weather.description": "Configure weather location, Xiaomi weather preview, and startup positioning behavior.",
|
||||
"settings.weather.location_source_header": "Location Source",
|
||||
@@ -255,7 +260,6 @@
|
||||
"settings.color.use_system_chrome_toggle": "Use system window chrome",
|
||||
"settings.color.theme_color_label": "Theme accent color",
|
||||
"settings.appearance.theme_color_mode_label": "Theme color source",
|
||||
"settings.appearance.system_material_label": "System material",
|
||||
"settings.appearance.theme_color_mode.neutral": "Default neutral",
|
||||
"settings.appearance.theme_color_mode.user": "User theme color Monet",
|
||||
"settings.appearance.theme_color_mode.wallpaper": "Wallpaper Monet",
|
||||
@@ -265,6 +269,8 @@
|
||||
"settings.appearance.theme_color_preview.app": "Currently previewing colors extracted from the app wallpaper.",
|
||||
"settings.appearance.theme_color_preview.system": "Currently previewing colors extracted from the system wallpaper.",
|
||||
"settings.appearance.theme_color_preview.fallback": "No usable wallpaper was found. The app is using a fallback accent.",
|
||||
"component.color_scheme.follow_system": "Follow system color scheme",
|
||||
"component.color_scheme.native": "Use component custom color scheme",
|
||||
"settings.appearance.system_material.none": "None",
|
||||
"settings.appearance.system_material.mica": "Mica",
|
||||
"settings.appearance.system_material.acrylic": "Acrylic",
|
||||
@@ -553,6 +559,7 @@
|
||||
"component_category.info": "Info",
|
||||
"component_category.calculator": "Calculator",
|
||||
"component_category.study": "Study",
|
||||
"component_category.file": "File",
|
||||
"component.date": "Calendar",
|
||||
"component.month_calendar": "Month Calendar",
|
||||
"component.lunar_calendar": "Lunar Calendar",
|
||||
@@ -580,6 +587,7 @@
|
||||
"component.whiteboard": "Blackboard (Portrait)",
|
||||
"component.blackboard_landscape": "Blackboard (Landscape)",
|
||||
"component.browser": "Browser",
|
||||
"component.office_recent_documents": "Recent Documents",
|
||||
"component.holiday_calendar": "Holiday Calendar",
|
||||
"component.study_environment": "Environment",
|
||||
"component.study_session_control": "Study Session Control",
|
||||
@@ -885,5 +893,7 @@
|
||||
"placement.tile": "Tile",
|
||||
"single_instance.notice.title": "App already running",
|
||||
"single_instance.notice.description": "The app is already running. There is no need to click multiple times to open it.",
|
||||
"single_instance.notice.button": "OK"
|
||||
"single_instance.notice.button": "OK",
|
||||
"market.status.install_success_restart_format": "✓ Plugin '{0}' installed successfully! Please restart the application to activate it.",
|
||||
"market.dialog.restart_message_format": "Plugin '{0}' has been installed successfully.\n\nTo use this plugin, you need to restart the application now.\n\nWould you like to restart?"
|
||||
}
|
||||
|
||||
@@ -31,13 +31,14 @@
|
||||
"settings.nav.plugins": "插件",
|
||||
"settings.nav.about": "关于",
|
||||
"settings.wallpaper.title": "壁纸",
|
||||
"settings.wallpaper.description": "选择图片或视频后可立即设为应用窗口壁纸。",
|
||||
"settings.wallpaper.description": "选择图片后可立即设为应用窗口壁纸。",
|
||||
"settings.wallpaper.current_label": "当前壁纸",
|
||||
"settings.wallpaper.type_label": "壁纸类型",
|
||||
"settings.wallpaper.type.image": "图片",
|
||||
"settings.wallpaper.type.video": "视频",
|
||||
"settings.wallpaper.type.solid_color": "纯色",
|
||||
"settings.wallpaper.color_label": "壁纸颜色",
|
||||
"settings.wallpaper.custom_color_tooltip": "自定义颜色",
|
||||
"settings.wallpaper.custom_color_apply": "应用",
|
||||
"settings.wallpaper.placement_label": "显示方式",
|
||||
"settings.wallpaper.placement_desc": "调整图像在桌面上的填充方式。",
|
||||
"settings.wallpaper.pick_button": "选择文件",
|
||||
@@ -46,20 +47,14 @@
|
||||
"settings.wallpaper.storage_unavailable": "存储提供器不可用。",
|
||||
"settings.wallpaper.import_failed": "导入壁纸文件失败。",
|
||||
"settings.wallpaper.image_applied": "图片壁纸已应用。",
|
||||
"settings.wallpaper.video_applied": "视频壁纸已应用。",
|
||||
"settings.wallpaper.unsupported_file": "所选文件类型不受支持。",
|
||||
"settings.wallpaper.apply_failed_format": "应用壁纸失败:{0}",
|
||||
"settings.wallpaper.mode_format": "壁纸模式:{0}。",
|
||||
"settings.wallpaper.video_mode": "视频壁纸使用自动填充模式。",
|
||||
"settings.wallpaper.cleared": "背景已恢复为纯色。",
|
||||
"settings.wallpaper.default_status": "当前使用纯色背景。",
|
||||
"settings.wallpaper.saved_not_found": "未找到已保存的壁纸文件,已使用纯色背景。",
|
||||
"settings.wallpaper.restored": "已恢复保存的壁纸。",
|
||||
"settings.wallpaper.video_restored": "已恢复保存的视频壁纸。",
|
||||
"settings.wallpaper.restore_failed": "恢复已保存壁纸失败,已使用纯色背景。",
|
||||
"settings.wallpaper.video_not_found": "未找到视频壁纸文件。",
|
||||
"settings.wallpaper.video_player_unavailable": "视频播放器不可用。",
|
||||
"settings.wallpaper.video_play_failed_format": "播放视频壁纸失败:{0}",
|
||||
"settings.grid.title": "网格布局",
|
||||
"settings.grid.description": "每个组件至少占用一个格子(最小 1x1)。",
|
||||
"settings.grid.short_side_label": "短边格数",
|
||||
@@ -85,7 +80,6 @@
|
||||
"settings.color.theme_ready_format": "主题色已就绪:{0}。",
|
||||
"settings.color.theme_applied_format": "{0}主题色已应用:{1}。",
|
||||
"settings.color.theme_updated_wallpaper": "壁纸已更新,莫奈色已刷新。",
|
||||
"settings.color.theme_updated_video": "视频壁纸已更新,主题色已刷新。",
|
||||
"settings.color.theme_cleared_wallpaper": "壁纸已清除,莫奈色已刷新。",
|
||||
"settings.status_bar.title": "状态栏",
|
||||
"settings.status_bar.description": "选择顶部状态栏显示的组件。",
|
||||
@@ -104,6 +98,11 @@
|
||||
"settings.privacy.crash_upload_description": "帮助我们提高应用稳定性。",
|
||||
"settings.privacy.usage_upload_title": "匿名上传使用数据",
|
||||
"settings.privacy.usage_upload_description": "帮助我们改善应用功能。",
|
||||
"settings.privacy.device_id_title": "设备标识符",
|
||||
"settings.privacy.device_id_description": "此设备的唯一标识符。点击刷新以重新生成。",
|
||||
"settings.privacy.refresh_device_id": "刷新",
|
||||
"settings.privacy.policy_hint_prefix": "了解更多详情,请",
|
||||
"settings.privacy.view_policy": "查看我们的隐私政策",
|
||||
"settings.weather.title": "天气",
|
||||
"settings.weather.description": "配置天气位置、小米天气预览和启动时的位置刷新行为。",
|
||||
"settings.weather.location_source_header": "位置来源",
|
||||
@@ -260,7 +259,6 @@
|
||||
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
|
||||
"settings.color.theme_color_label": "主题强调色",
|
||||
"settings.appearance.theme_color_mode_label": "主题色来源",
|
||||
"settings.appearance.system_material_label": "系统材质",
|
||||
"settings.appearance.theme_color_mode.neutral": "默认中性",
|
||||
"settings.appearance.theme_color_mode.user": "用户主题色 Monet",
|
||||
"settings.appearance.theme_color_mode.wallpaper": "壁纸 Monet 取色",
|
||||
@@ -270,6 +268,8 @@
|
||||
"settings.appearance.theme_color_preview.app": "当前正在预览从应用壁纸提取的颜色。",
|
||||
"settings.appearance.theme_color_preview.system": "当前正在预览从系统壁纸提取的颜色。",
|
||||
"settings.appearance.theme_color_preview.fallback": "没有可用壁纸,当前使用回退强调色。",
|
||||
"component.color_scheme.follow_system": "跟随系统配色",
|
||||
"component.color_scheme.native": "使用组件自定义配色",
|
||||
"settings.appearance.system_material.none": "无",
|
||||
"settings.appearance.system_material.mica": "Mica",
|
||||
"settings.appearance.system_material.acrylic": "Acrylic",
|
||||
@@ -392,7 +392,6 @@
|
||||
"settings.footer": "LanMountainDesktop 设置",
|
||||
"filepicker.title": "选择壁纸",
|
||||
"filepicker.image_files": "图片文件",
|
||||
"filepicker.video_files": "视频文件",
|
||||
"common.day": "日间",
|
||||
"common.night": "夜间",
|
||||
"common.back": "返回",
|
||||
@@ -558,6 +557,7 @@
|
||||
"component_category.info": "信息推荐",
|
||||
"component_category.calculator": "计算器",
|
||||
"component_category.study": "自习",
|
||||
"component_category.file": "文件",
|
||||
"component.date": "日历",
|
||||
"component.month_calendar": "月历",
|
||||
"component.lunar_calendar": "农历",
|
||||
@@ -585,6 +585,7 @@
|
||||
"component.whiteboard": "竖向小黑板",
|
||||
"component.blackboard_landscape": "横向小黑板",
|
||||
"component.browser": "浏览器",
|
||||
"component.office_recent_documents": "最近文档",
|
||||
"component.holiday_calendar": "节假日日历",
|
||||
"component.study_environment": "环境",
|
||||
"component.study_session_control": "自习时段控制",
|
||||
@@ -890,5 +891,7 @@
|
||||
"placement.tile": "平铺",
|
||||
"single_instance.notice.title": "应用已经运行",
|
||||
"single_instance.notice.description": "应用已经运行,无需多次点击打开。",
|
||||
"single_instance.notice.button": "确定"
|
||||
"single_instance.notice.button": "确定",
|
||||
"market.status.install_success_restart_format": "✓ 插件'{0}'安装成功!请重启应用以激活它。",
|
||||
"market.dialog.restart_message_format": "插件'{0}'已成功安装。\n\n要使用此插件,您需要立即重启应用。\n\n是否立即重启?"
|
||||
}
|
||||
|
||||
@@ -62,8 +62,6 @@ public sealed class AppSettingsSnapshot
|
||||
|
||||
public string AppRenderMode { get; set; } = "Default";
|
||||
|
||||
public bool AutoCheckUpdates { get; set; } = true;
|
||||
|
||||
public bool IncludePrereleaseUpdates { get; set; }
|
||||
|
||||
public bool UploadAnonymousCrashData { get; set; }
|
||||
@@ -72,6 +70,8 @@ public sealed class AppSettingsSnapshot
|
||||
|
||||
public string? DeviceId { get; set; }
|
||||
|
||||
public string? PersistentUserId { get; set; }
|
||||
|
||||
public string UpdateChannel { get; set; } = "stable";
|
||||
|
||||
public string UpdateMode { get; set; } = "download_then_confirm";
|
||||
|
||||
@@ -6,6 +6,8 @@ public sealed class ComponentSettingsSnapshot
|
||||
{
|
||||
public string DailyArtworkMirrorSource { get; set; } = DailyArtworkMirrorSources.Overseas;
|
||||
|
||||
public string? ColorSchemeSource { get; set; }
|
||||
|
||||
public List<ImportedClassScheduleSnapshot> ImportedClassSchedules { get; set; } = [];
|
||||
|
||||
public string ActiveImportedClassScheduleId { get; set; } = string.Empty;
|
||||
|
||||
@@ -15,7 +15,6 @@ using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Theme;
|
||||
using LibVLCSharp.Shared;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
@@ -89,11 +88,6 @@ internal interface IMaterialSurfaceService
|
||||
AppearanceMaterialSurface GetSurface(ThemeColorContext context, MaterialSurfaceRole role);
|
||||
}
|
||||
|
||||
internal interface IVideoWallpaperSeedExtractor
|
||||
{
|
||||
IReadOnlyList<Color> ExtractSeedCandidates(string videoPath, MonetColorService monetColorService);
|
||||
}
|
||||
|
||||
internal readonly record struct WallpaperSeedSourceDescriptor(
|
||||
string SourceKind,
|
||||
string SourceKey,
|
||||
@@ -114,75 +108,6 @@ internal readonly record struct WallpaperPaletteResolution(
|
||||
Color EffectiveSeedColor,
|
||||
string? ResolvedWallpaperPath);
|
||||
|
||||
internal sealed class LibVlcVideoWallpaperSeedExtractor : IVideoWallpaperSeedExtractor
|
||||
{
|
||||
public IReadOnlyList<Color> ExtractSeedCandidates(string videoPath, MonetColorService monetColorService)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(videoPath) || !File.Exists(videoPath))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var snapshotPath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"lanmountaindesktop-video-seed-{Guid.NewGuid():N}.png");
|
||||
|
||||
try
|
||||
{
|
||||
using var libVlc = new LibVLC("--no-audio", "--intf=dummy", "--no-video-title-show");
|
||||
using var media = new Media(libVlc, new Uri(videoPath));
|
||||
using var mediaPlayer = new MediaPlayer(libVlc)
|
||||
{
|
||||
Media = media
|
||||
};
|
||||
|
||||
mediaPlayer.Play();
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
while (stopwatch.Elapsed < TimeSpan.FromSeconds(5))
|
||||
{
|
||||
Thread.Sleep(180);
|
||||
if (!mediaPlayer.TakeSnapshot(0, snapshotPath, 320, 180))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(snapshotPath);
|
||||
if (!fileInfo.Exists || fileInfo.Length <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using var bitmap = new Bitmap(snapshotPath);
|
||||
return monetColorService.ExtractSeedCandidates(bitmap);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn(
|
||||
"Appearance.VideoWallpaperPalette",
|
||||
$"Failed to extract wallpaper seed candidates from video '{videoPath}'.",
|
||||
ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(snapshotPath))
|
||||
{
|
||||
File.Delete(snapshotPath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort cleanup only.
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SystemWallpaperService : ISystemWallpaperService
|
||||
{
|
||||
public bool IsSupported => OperatingSystem.IsWindows();
|
||||
@@ -248,6 +173,15 @@ internal sealed class WindowMaterialService : IWindowMaterialService
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(window);
|
||||
|
||||
var normalizedMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(materialMode);
|
||||
|
||||
if (normalizedMode == ThemeAppearanceValues.MaterialNone)
|
||||
{
|
||||
window.Background = Brushes.White;
|
||||
window.TransparencyLevelHint = [WindowTransparencyLevel.None];
|
||||
return;
|
||||
}
|
||||
|
||||
window.Background = Brushes.Transparent;
|
||||
|
||||
if (!OperatingSystem.IsWindows() || !IsTransparencyEnabled())
|
||||
@@ -259,7 +193,6 @@ internal sealed class WindowMaterialService : IWindowMaterialService
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(materialMode);
|
||||
window.TransparencyLevelHint = normalizedMode switch
|
||||
{
|
||||
ThemeAppearanceValues.MaterialMica =>
|
||||
@@ -469,7 +402,6 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
|
||||
private readonly ISystemWallpaperService _systemWallpaperService;
|
||||
private readonly IWindowMaterialService _windowMaterialService;
|
||||
private readonly IMaterialSurfaceService _materialSurfaceService;
|
||||
private readonly IVideoWallpaperSeedExtractor _videoWallpaperSeedExtractor;
|
||||
private readonly MonetColorService _monetColorService = new();
|
||||
private readonly string _liveThemeColorMode;
|
||||
private readonly string _liveSystemMaterialMode;
|
||||
@@ -482,14 +414,12 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
|
||||
ISettingsFacadeService settingsFacade,
|
||||
ISystemWallpaperService systemWallpaperService,
|
||||
IWindowMaterialService windowMaterialService,
|
||||
IMaterialSurfaceService materialSurfaceService,
|
||||
IVideoWallpaperSeedExtractor? videoWallpaperSeedExtractor = null)
|
||||
IMaterialSurfaceService materialSurfaceService)
|
||||
{
|
||||
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
|
||||
_systemWallpaperService = systemWallpaperService ?? throw new ArgumentNullException(nameof(systemWallpaperService));
|
||||
_windowMaterialService = windowMaterialService ?? throw new ArgumentNullException(nameof(windowMaterialService));
|
||||
_materialSurfaceService = materialSurfaceService ?? throw new ArgumentNullException(nameof(materialSurfaceService));
|
||||
_videoWallpaperSeedExtractor = videoWallpaperSeedExtractor ?? new LibVlcVideoWallpaperSeedExtractor();
|
||||
var initialThemeState = _settingsFacade.Theme.Get();
|
||||
_liveThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(
|
||||
initialThemeState.ThemeColorMode,
|
||||
@@ -878,7 +808,6 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
|
||||
IReadOnlyList<Color> seedCandidates = source.SourceKind switch
|
||||
{
|
||||
"app_wallpaper" or "system_wallpaper" => ExtractImageSeedCandidates(source.FilePath),
|
||||
"app_video" => ExtractVideoSeedCandidates(source.FilePath),
|
||||
"app_solid" when source.SolidColor is { } solidColor => new[] { solidColor },
|
||||
_ => []
|
||||
};
|
||||
@@ -912,16 +841,6 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<Color> ExtractVideoSeedCandidates(string? wallpaperPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(wallpaperPath) || !File.Exists(wallpaperPath))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return _videoWallpaperSeedExtractor.ExtractSeedCandidates(wallpaperPath, _monetColorService);
|
||||
}
|
||||
|
||||
private WallpaperSeedSourceDescriptor ResolveWallpaperSeedSource(WallpaperSettingsState wallpaperState)
|
||||
{
|
||||
if (string.Equals(wallpaperState.Type, "SolidColor", StringComparison.OrdinalIgnoreCase) &&
|
||||
@@ -952,16 +871,6 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa
|
||||
wallpaperPath,
|
||||
null);
|
||||
}
|
||||
|
||||
if (appWallpaperMediaType == WallpaperMediaType.Video)
|
||||
{
|
||||
return new WallpaperSeedSourceDescriptor(
|
||||
"app_video",
|
||||
CreateWallpaperSourceKey("app_video", wallpaperPath),
|
||||
wallpaperPath,
|
||||
wallpaperPath,
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
var systemWallpaper = _systemWallpaperService.GetWallpaperPath();
|
||||
|
||||
@@ -163,7 +163,7 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
|
||||
var totalElapsedWeeks = (int)Math.Floor(
|
||||
(referenceDate.ToDateTime(TimeOnly.MinValue) - cycleRule.SingleWeekStartDate.Value.ToDateTime(TimeOnly.MinValue)).TotalDays / 7d);
|
||||
|
||||
for (var cycleLength = 2; cycleLength <= maxCycle; cycleLength++)
|
||||
for (var cycleLength = 1; cycleLength <= maxCycle; cycleLength++)
|
||||
{
|
||||
var cycleOffset = cycleLength < cycleRule.MultiWeekRotationOffset.Count
|
||||
? cycleRule.MultiWeekRotationOffset[cycleLength]
|
||||
@@ -668,7 +668,7 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
|
||||
return true;
|
||||
}
|
||||
|
||||
if (weekCountDivTotal <= 1 || weekCountDivTotal >= cyclePositions.Count)
|
||||
if (weekCountDivTotal <= 0 || weekCountDivTotal >= cyclePositions.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public sealed class DeviceIdService
|
||||
{
|
||||
private static DeviceIdService? _instance;
|
||||
private string? _deviceId;
|
||||
private string? _persistentUserId; // 持久化的用户ID,用于关联设备
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private bool _isInitialized;
|
||||
|
||||
@@ -43,6 +44,19 @@ public sealed class DeviceIdService
|
||||
}
|
||||
}
|
||||
|
||||
// 持久化的用户ID,用于跨设备关联用户
|
||||
public string PersistentUserId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_persistentUserId is null)
|
||||
{
|
||||
throw new InvalidOperationException("PersistentUserId not initialized");
|
||||
}
|
||||
return _persistentUserId;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureDeviceId()
|
||||
{
|
||||
if (_isInitialized)
|
||||
@@ -56,13 +70,22 @@ public sealed class DeviceIdService
|
||||
{
|
||||
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
|
||||
// 初始化或生成持久化用户ID(只生成一次,永不改变)
|
||||
if (string.IsNullOrEmpty(snapshot.PersistentUserId))
|
||||
{
|
||||
snapshot.PersistentUserId = GeneratePersistentUserId();
|
||||
AppLogger.Info("DeviceId", $"Generated new persistent user ID: {snapshot.PersistentUserId}");
|
||||
}
|
||||
_persistentUserId = snapshot.PersistentUserId;
|
||||
|
||||
// 初始化或生成设备ID(可以刷新)
|
||||
if (string.IsNullOrEmpty(snapshot.DeviceId))
|
||||
{
|
||||
snapshot.DeviceId = GenerateDeviceId();
|
||||
_settingsFacade.Settings.SaveSnapshot(
|
||||
SettingsScope.App,
|
||||
snapshot,
|
||||
changedKeys: [nameof(AppSettingsSnapshot.DeviceId)]);
|
||||
changedKeys: [nameof(AppSettingsSnapshot.DeviceId), nameof(AppSettingsSnapshot.PersistentUserId)]);
|
||||
_deviceId = snapshot.DeviceId;
|
||||
AppLogger.Info("DeviceId", $"Generated new device ID: {_deviceId}");
|
||||
}
|
||||
@@ -75,6 +98,7 @@ public sealed class DeviceIdService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_deviceId = GenerateDeviceId();
|
||||
_persistentUserId = GeneratePersistentUserId();
|
||||
AppLogger.Warn("DeviceId", $"Failed to persist device ID, using generated ID: {_deviceId}", ex);
|
||||
}
|
||||
}
|
||||
@@ -87,6 +111,15 @@ public sealed class DeviceIdService
|
||||
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(deviceInfo));
|
||||
return Convert.ToHexString(hash)[..32].ToLower();
|
||||
}
|
||||
|
||||
private static string GeneratePersistentUserId()
|
||||
{
|
||||
// 生成一个永久性的用户ID,基于机器名和用户名的哈希
|
||||
var userInfo = $"{Environment.MachineName}|{Environment.UserName}|LanMountainDesktop";
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(userInfo));
|
||||
return Convert.ToHexString(hash)[..32].ToLower();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UserBehaviorAnalyticsService : IDisposable
|
||||
@@ -140,9 +173,18 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromSeconds(30));
|
||||
|
||||
// 发送PostHog标准的$pageview事件用于统计日活(始终发送,不受开关影响)
|
||||
CaptureEvent("$pageview", new Dictionary<string, object>
|
||||
{
|
||||
{ "$current_url", "app://main" },
|
||||
{ "$title", "LanMountainDesktop" }
|
||||
});
|
||||
|
||||
// 发送应用启动事件(始终发送,用于统计用户数量)
|
||||
CaptureEvent("app_online", new Dictionary<string, object>
|
||||
{
|
||||
{ "event_type", "app_start" }
|
||||
{ "event_type", "app_start" },
|
||||
{ "analytics_enabled", _isEnabled }
|
||||
});
|
||||
|
||||
AppLogger.Info("UserBehaviorAnalytics", $"Analytics initialized. DeviceId={_deviceIdService.DeviceId}, Enabled={_isEnabled}");
|
||||
@@ -382,10 +424,22 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
// 基础事件($pageview, app_online, app_shutdown等)始终发送,用于统计用户数量
|
||||
bool isBasicEvent = eventName.StartsWith("$") ||
|
||||
eventName == "app_online" ||
|
||||
eventName == "app_shutdown" ||
|
||||
eventName == "$identify";
|
||||
|
||||
// 非基础事件只有在启用时才发送
|
||||
if (!isBasicEvent && !_isEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventData = new UserBehaviorEvent
|
||||
{
|
||||
Event = eventName,
|
||||
DistinctId = _deviceIdService.DeviceId,
|
||||
DistinctId = _deviceIdService.PersistentUserId, // 使用持久化用户ID
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Properties = properties ?? new Dictionary<string, object>(),
|
||||
IncludeDetailedData = _isEnabled
|
||||
@@ -542,21 +596,29 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
|
||||
{
|
||||
var userProperties = new Dictionary<string, object>
|
||||
{
|
||||
{ "$device_id", distinctId },
|
||||
{ "$app_version", GetAppVersion() },
|
||||
{ "$os", GetOsName() },
|
||||
{ "$os_version", GetOsVersion() }
|
||||
{ "$os_version", GetOsVersion() },
|
||||
{ "$device_type", GetDeviceModel() },
|
||||
{ "$device_id", _deviceIdService.DeviceId } // 当前设备ID
|
||||
};
|
||||
|
||||
// PostHog正确的$identify格式
|
||||
// 使用PersistentUserId作为distinct_id,确保设备ID刷新后仍能关联到同一用户
|
||||
var requestBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "api_key", PostHogApiKey },
|
||||
{ "event", "$identify" },
|
||||
{ "distinct_id", _deviceIdService.PersistentUserId }, // 使用持久化用户ID
|
||||
{ "timestamp", DateTimeOffset.UtcNow.ToString("o") },
|
||||
{ "properties", new Dictionary<string, object>
|
||||
{
|
||||
{ "distinct_id", distinctId },
|
||||
{ "$set", userProperties }
|
||||
{ "$set", userProperties },
|
||||
{ "$set_once", new Dictionary<string, object>
|
||||
{
|
||||
{ "first_app_open", DateTimeOffset.UtcNow.ToString("o") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -796,6 +858,9 @@ public sealed class CrashReportService
|
||||
});
|
||||
|
||||
ConfigureCrashReportingScope();
|
||||
|
||||
// 显式开始会话跟踪
|
||||
SentrySdk.StartSession();
|
||||
|
||||
AppLogger.Info("CrashReport", $"Sentry crash reporting initialized. DeviceId={_deviceIdService.DeviceId}");
|
||||
|
||||
@@ -849,7 +914,10 @@ public sealed class CrashReportService
|
||||
{
|
||||
if (_isEnabled && _isInitialized)
|
||||
{
|
||||
AppLogger.Info("CrashReport", $"Shutdown event will be sent via Sentry. DeviceId={_deviceIdService.DeviceId}");
|
||||
// 结束Sentry会话
|
||||
SentrySdk.EndSession();
|
||||
SentrySdk.Flush(TimeSpan.FromSeconds(3));
|
||||
AppLogger.Info("CrashReport", $"Shutdown event sent via Sentry. DeviceId={_deviceIdService.DeviceId}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
@@ -16,6 +17,23 @@ public sealed class LocalizationService
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _cache =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// 清除指定语言代码的缓存,强制下次重新加载。
|
||||
/// 在语言切换时调用此方法以确保加载最新的语言文件。
|
||||
/// </summary>
|
||||
public void ClearCache(string? languageCode = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(languageCode))
|
||||
{
|
||||
_cache.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
var normalizedCode = NormalizeLanguageCode(languageCode);
|
||||
_cache.Remove(normalizedCode);
|
||||
}
|
||||
}
|
||||
|
||||
public string NormalizeLanguageCode(string? languageCode)
|
||||
{
|
||||
return string.Equals(languageCode, "en-US", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -42,14 +60,17 @@ public sealed class LocalizationService
|
||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
var filePath = Path.Combine(AppContext.BaseDirectory, "Localization", $"{languageCode}.json");
|
||||
if (File.Exists(filePath))
|
||||
var json = TryLoadFromFileSystem(languageCode);
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
json = TryLoadFromEmbeddedResource(languageCode);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
// Defensive: tolerate accidentally duplicated UTF-8 BOM characters at file start.
|
||||
json = json.TrimStart('\uFEFF');
|
||||
var data = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonOptions);
|
||||
if (data is not null)
|
||||
if (data is not null && data.Count > 0)
|
||||
{
|
||||
result = new Dictionary<string, string>(data, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -60,7 +81,48 @@ public sealed class LocalizationService
|
||||
// Keep empty table for resilience.
|
||||
}
|
||||
|
||||
_cache[languageCode] = result;
|
||||
// 只有当语言表非空时才缓存,这样如果加载失败可以下次重试
|
||||
if (result.Count > 0)
|
||||
{
|
||||
_cache[languageCode] = result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private string? TryLoadFromFileSystem(string languageCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = Path.Combine(AppContext.BaseDirectory, "Localization", $"{languageCode}.json");
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
return File.ReadAllText(filePath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Continue to next method
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? TryLoadFromEmbeddedResource(string languageCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = $"LanMountainDesktop.Localization.{languageCode}.json";
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream != null)
|
||||
{
|
||||
using var reader = new StreamReader(stream);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Continue to next method
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
338
LanMountainDesktop/Services/OfficeRecentDocumentsService.cs
Normal file
338
LanMountainDesktop/Services/OfficeRecentDocumentsService.cs
Normal file
@@ -0,0 +1,338 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public interface IOfficeRecentDocumentsService
|
||||
{
|
||||
List<OfficeRecentDocument> GetRecentDocuments(int maxCount = 20);
|
||||
void OpenDocument(string filePath);
|
||||
}
|
||||
|
||||
public sealed class OfficeRecentDocument
|
||||
{
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public string Extension { get; set; } = string.Empty;
|
||||
public DateTime LastModifiedTime { get; set; }
|
||||
public long FileSizeBytes { get; set; }
|
||||
public string IconGlyph { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService
|
||||
{
|
||||
private static readonly string[] OfficeExtensions = { ".doc", ".docx", ".dot", ".dotx", ".rtf" };
|
||||
private static readonly string[] ExcelExtensions = { ".xls", ".xlsx", ".xlsm", ".xlsb", ".csv" };
|
||||
private static readonly string[] PowerPointExtensions = { ".ppt", ".pptx", ".pptm", ".pps", ".ppsx" };
|
||||
|
||||
public List<OfficeRecentDocument> GetRecentDocuments(int maxCount = 20)
|
||||
{
|
||||
var documents = new List<OfficeRecentDocument>();
|
||||
|
||||
// 方法1: 从注册表读取Office最近文档(最可靠)
|
||||
TryGetFromRegistry(documents);
|
||||
|
||||
// 方法2: 从Recent文件夹读取快捷方式(备用)
|
||||
TryGetFromRecentFolders(documents);
|
||||
|
||||
// 方法3: 从Windows Jump List读取(如果可用)
|
||||
TryGetFromJumpList(documents);
|
||||
|
||||
return documents
|
||||
.GroupBy(d => d.FilePath, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.OrderByDescending(d => d.LastModifiedTime).First())
|
||||
.OrderByDescending(d => d.LastModifiedTime)
|
||||
.Take(maxCount)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public void OpenDocument(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = filePath,
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CA1416 // 平台兼容性警告
|
||||
private void TryGetFromRegistry(List<OfficeRecentDocument> documents)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Word最近文档
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\Word\Reading Locations");
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Word\Reading Locations");
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\15.0\Word\Reading Locations");
|
||||
|
||||
// Excel最近文档
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\Excel\Reading Locations");
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Excel\Reading Locations");
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\15.0\Excel\Reading Locations");
|
||||
|
||||
// PowerPoint最近文档
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\PowerPoint\Reading Locations");
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\PowerPoint\Reading Locations");
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\15.0\PowerPoint\Reading Locations");
|
||||
|
||||
// 通用Office最近文档
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Common\Open Find\Microsoft Office Word");
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Common\Open Find\Microsoft Office Excel");
|
||||
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Common\Open Find\Microsoft Office PowerPoint");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略注册表访问错误
|
||||
}
|
||||
}
|
||||
|
||||
private void TryGetFromOfficeRegistry(List<OfficeRecentDocument> documents, string registryPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(registryPath);
|
||||
if (key == null) return;
|
||||
|
||||
foreach (var subKeyName in key.GetSubKeyNames())
|
||||
{
|
||||
try
|
||||
{
|
||||
using var subKey = key.OpenSubKey(subKeyName);
|
||||
if (subKey == null) continue;
|
||||
|
||||
var filePath = subKey.GetValue("Path") as string;
|
||||
if (string.IsNullOrEmpty(filePath)) continue;
|
||||
|
||||
AddDocumentIfExists(documents, filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略单个子键访问错误
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略注册表访问错误
|
||||
}
|
||||
}
|
||||
#pragma warning restore CA1416 // 平台兼容性警告
|
||||
|
||||
private void TryGetFromRecentFolders(List<OfficeRecentDocument> documents)
|
||||
{
|
||||
var recentPaths = GetRecentFolders();
|
||||
|
||||
foreach (var recentPath in recentPaths)
|
||||
{
|
||||
if (!Directory.Exists(recentPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var files = Directory.GetFiles(recentPath, "*.lnk");
|
||||
foreach (var lnkPath in files)
|
||||
{
|
||||
var targetPath = GetShortcutTarget(lnkPath);
|
||||
if (string.IsNullOrEmpty(targetPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddDocumentIfExists(documents, targetPath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略文件夹访问错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TryGetFromJumpList(List<OfficeRecentDocument> documents)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Windows Jump List存储在以下位置
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var jumpListPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Microsoft", "Windows", "Recent", "AutomaticDestinations");
|
||||
|
||||
if (!Directory.Exists(jumpListPath)) return;
|
||||
|
||||
// Office应用的Jump List文件
|
||||
var officeJumpListFiles = new[]
|
||||
{
|
||||
"a7bd7a3f3d5a4c74.automaticDestinations-ms", // Word
|
||||
"9b524fe3be704a4d.automaticDestinations-ms", // Excel
|
||||
"d0063c4c7de64e5e.automaticDestinations-ms" // PowerPoint
|
||||
};
|
||||
|
||||
foreach (var jumpFile in officeJumpListFiles)
|
||||
{
|
||||
var fullPath = Path.Combine(jumpListPath, jumpFile);
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
TryParseJumpListFile(fullPath, documents);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Jump List解析失败,忽略
|
||||
}
|
||||
}
|
||||
|
||||
private void TryParseJumpListFile(string jumpListPath, List<OfficeRecentDocument> documents)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Jump List文件是二进制格式,这里使用简化的方法
|
||||
// 读取文件并尝试提取文件路径
|
||||
var bytes = File.ReadAllBytes(jumpListPath);
|
||||
var text = Encoding.Unicode.GetString(bytes);
|
||||
|
||||
// 查找可能的文件路径(简化实现)
|
||||
var possiblePaths = ExtractPossiblePaths(text);
|
||||
foreach (var path in possiblePaths)
|
||||
{
|
||||
AddDocumentIfExists(documents, path);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Jump List解析失败,忽略
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<string> ExtractPossiblePaths(string text)
|
||||
{
|
||||
var paths = new List<string>();
|
||||
|
||||
// 查找常见的文件路径模式
|
||||
var patterns = new[]
|
||||
{
|
||||
@"[A-Z]:\\[^\x00-\x1F""<>|]*\.(docx?|xlsx?|pptx?|rtf|csv)",
|
||||
@"\\\\[^\\]+\\[^\x00-\x1F""<>|]*\.(docx?|xlsx?|pptx?|rtf|csv)"
|
||||
};
|
||||
|
||||
foreach (var pattern in patterns)
|
||||
{
|
||||
try
|
||||
{
|
||||
var matches = System.Text.RegularExpressions.Regex.Matches(text, pattern,
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
|
||||
foreach (System.Text.RegularExpressions.Match match in matches)
|
||||
{
|
||||
var path = match.Value.Trim('\0', ' ', '"');
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
paths.Add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略正则表达式错误
|
||||
}
|
||||
}
|
||||
|
||||
return paths.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AddDocumentIfExists(List<OfficeRecentDocument> documents, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var extension = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
if (!IsOfficeFile(extension))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
var doc = new OfficeRecentDocument
|
||||
{
|
||||
FileName = Path.GetFileNameWithoutExtension(filePath),
|
||||
FilePath = filePath,
|
||||
Extension = extension,
|
||||
LastModifiedTime = fileInfo.LastWriteTime,
|
||||
FileSizeBytes = fileInfo.Length,
|
||||
IconGlyph = GetIconGlyph(extension)
|
||||
};
|
||||
|
||||
if (!documents.Any(d => string.Equals(d.FilePath, filePath, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
documents.Add(doc);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略单个文件处理错误
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> GetRecentFolders()
|
||||
{
|
||||
var folders = new List<string>();
|
||||
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
folders.Add(Path.Combine(appData, "Microsoft", "Word", "Recent"));
|
||||
folders.Add(Path.Combine(appData, "Microsoft", "Excel", "Recent"));
|
||||
folders.Add(Path.Combine(appData, "Microsoft", "PowerPoint", "Recent"));
|
||||
|
||||
// 添加Office 365路径
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
folders.Add(Path.Combine(localAppData, "Microsoft", "Office", "Word", "Recent"));
|
||||
folders.Add(Path.Combine(localAppData, "Microsoft", "Office", "Excel", "Recent"));
|
||||
folders.Add(Path.Combine(localAppData, "Microsoft", "Office", "PowerPoint", "Recent"));
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
private static bool IsOfficeFile(string extension)
|
||||
{
|
||||
return OfficeExtensions.Contains(extension) ||
|
||||
ExcelExtensions.Contains(extension) ||
|
||||
PowerPointExtensions.Contains(extension);
|
||||
}
|
||||
|
||||
private static string GetIconGlyph(string extension)
|
||||
{
|
||||
return extension switch
|
||||
{
|
||||
".doc" or ".docx" or ".dot" or ".dotx" or ".rtf" => "\uE8A5",
|
||||
".xls" or ".xlsx" or ".xlsm" or ".xlsb" or ".csv" => "\uE9F9",
|
||||
".ppt" or ".pptx" or ".pptm" or ".pps" or ".ppsx" => "\uE8A1",
|
||||
_ => "\uE8A5"
|
||||
};
|
||||
}
|
||||
|
||||
private static string? GetShortcutTarget(string lnkPath)
|
||||
{
|
||||
return ShortcutHelper.GetShortcutTarget(lnkPath);
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,11 @@ namespace LanMountainDesktop.Services.Settings;
|
||||
public enum WallpaperMediaType
|
||||
{
|
||||
None,
|
||||
Image,
|
||||
Video
|
||||
Image
|
||||
}
|
||||
|
||||
public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent);
|
||||
public sealed record WallpaperSettingsState(string? WallpaperPath, string Type, string? Color, string Placement);
|
||||
public sealed record WallpaperSettingsState(string? WallpaperPath, string Type, string? Color, string Placement, string? CustomColor = null);
|
||||
public sealed record ThemeAppearanceSettingsState(
|
||||
bool IsNightMode,
|
||||
string? ThemeColor,
|
||||
@@ -48,7 +47,6 @@ public sealed record PrivacySettingsState(
|
||||
bool UploadAnonymousCrashData,
|
||||
bool UploadAnonymousUsageData);
|
||||
public sealed record UpdateSettingsState(
|
||||
bool AutoCheckUpdates,
|
||||
bool IncludePrereleaseUpdates,
|
||||
string UpdateChannel,
|
||||
string UpdateMode,
|
||||
|
||||
@@ -147,11 +147,6 @@ internal sealed class WallpaperMediaService : IWallpaperMediaService
|
||||
".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> VideoExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v"
|
||||
};
|
||||
|
||||
private readonly string _wallpapersDirectory;
|
||||
|
||||
public WallpaperMediaService()
|
||||
@@ -180,11 +175,6 @@ internal sealed class WallpaperMediaService : IWallpaperMediaService
|
||||
return WallpaperMediaType.Image;
|
||||
}
|
||||
|
||||
if (VideoExtensions.Contains(extension))
|
||||
{
|
||||
return WallpaperMediaType.Video;
|
||||
}
|
||||
|
||||
return WallpaperMediaType.None;
|
||||
}
|
||||
|
||||
@@ -638,7 +628,6 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
||||
snapshot.UpdateChannel,
|
||||
snapshot.IncludePrereleaseUpdates);
|
||||
return new UpdateSettingsState(
|
||||
snapshot.AutoCheckUpdates,
|
||||
string.Equals(normalizedChannel, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase),
|
||||
normalizedChannel,
|
||||
UpdateSettingsValues.NormalizeMode(snapshot.UpdateMode),
|
||||
@@ -656,7 +645,6 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
||||
var normalizedChannel = UpdateSettingsValues.NormalizeChannel(
|
||||
state.UpdateChannel,
|
||||
state.IncludePrereleaseUpdates);
|
||||
snapshot.AutoCheckUpdates = state.AutoCheckUpdates;
|
||||
snapshot.IncludePrereleaseUpdates = string.Equals(
|
||||
normalizedChannel,
|
||||
UpdateSettingsValues.ChannelPreview,
|
||||
@@ -682,7 +670,7 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
||||
snapshot,
|
||||
changedKeys:
|
||||
[
|
||||
nameof(AppSettingsSnapshot.AutoCheckUpdates),
|
||||
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
|
||||
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
|
||||
nameof(AppSettingsSnapshot.UpdateChannel),
|
||||
nameof(AppSettingsSnapshot.UpdateMode),
|
||||
|
||||
@@ -294,6 +294,8 @@ internal sealed class SettingsWindowService : ISettingsWindowService
|
||||
if (languageChanged)
|
||||
{
|
||||
var regionState = _settingsFacade.Region.Get();
|
||||
// 清除本地化缓存,强制重新加载语言文件
|
||||
_localizationService.ClearCache();
|
||||
_viewModel.RefreshLanguage(regionState.LanguageCode);
|
||||
_pageRegistry.Rebuild();
|
||||
_window.ReloadPages(_viewModel.CurrentPageId);
|
||||
|
||||
32
LanMountainDesktop/Services/ShortcutHelper.cs
Normal file
32
LanMountainDesktop/Services/ShortcutHelper.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
internal static class ShortcutHelper
|
||||
{
|
||||
[ComImport]
|
||||
[Guid("72C24DD5-D70A-438B-8A42-98424B88AFB8")]
|
||||
internal class WshShell { }
|
||||
|
||||
[ComImport]
|
||||
[Guid("F935DC21-1CF0-11D0-ADB9-00C04FD58A0B")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
|
||||
internal interface IWshShortcut
|
||||
{
|
||||
string TargetPath { get; set; }
|
||||
}
|
||||
|
||||
public static string? GetShortcutTarget(string lnkPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
dynamic shell = new WshShell();
|
||||
dynamic shortcut = shell.CreateShortcut(lnkPath);
|
||||
return shortcut.TargetPath;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ public static class ThemeAppearanceValues
|
||||
public const string ColorModeSeedMonet = "seed_monet";
|
||||
public const string ColorModeWallpaperMonet = "wallpaper_monet";
|
||||
|
||||
public const string ColorSchemeFollowSystem = "follow_system";
|
||||
public const string ColorSchemeNative = "native";
|
||||
|
||||
public const string MaterialNone = "none";
|
||||
public const string MaterialMica = "mica";
|
||||
public const string MaterialAcrylic = "acrylic";
|
||||
|
||||
@@ -131,13 +131,10 @@ public sealed class UpdateWorkflowService
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var state = _settingsFacade.Update.Get();
|
||||
if (!state.AutoCheckUpdates)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Always check for updates on startup (removed AutoCheckUpdates check)
|
||||
var result = await CheckForUpdatesAsync(currentVersion, cancellationToken);
|
||||
if (!result.Success || !result.IsUpdateAvailable || result.PreferredAsset is null)
|
||||
{
|
||||
@@ -145,12 +142,14 @@ public sealed class UpdateWorkflowService
|
||||
}
|
||||
|
||||
var normalizedMode = UpdateSettingsValues.NormalizeMode(state.UpdateMode);
|
||||
if (string.Equals(normalizedMode, UpdateSettingsValues.ModeManual, StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
// For "Silent Download" and "Silent Install" modes, automatically download the update
|
||||
if (string.Equals(normalizedMode, UpdateSettingsValues.ModeDownloadThenConfirm, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(normalizedMode, UpdateSettingsValues.ModeSilentOnExit, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
await DownloadReleaseAsync(result, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
await DownloadReleaseAsync(result, cancellationToken: cancellationToken);
|
||||
// For "Manual" mode, just check but don't download
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
@@ -564,11 +564,19 @@ public sealed partial class PluginMarketSettingsPageViewModel : ViewModelBase
|
||||
{
|
||||
RefreshInstalledSnapshot();
|
||||
RefreshItemStates();
|
||||
|
||||
// 设置更明显的状态消息
|
||||
var pluginName = result.PluginName ?? item.Name;
|
||||
StatusMessage = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
L("market.status.install_success_format", "Plugin '{0}' has been staged. Restart the app to apply it."),
|
||||
result.PluginName ?? item.Name);
|
||||
RestartRequested?.Invoke(RestartRequiredMessage);
|
||||
L("market.status.install_success_restart_format", "✓ Plugin '{0}' installed successfully! Please restart the application to activate it."),
|
||||
pluginName);
|
||||
|
||||
// 触发重启提醒
|
||||
RestartRequested?.Invoke(string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
L("market.dialog.restart_message_format", "Plugin '{0}' has been installed successfully.\n\nTo use this plugin, you need to restart the application now.\n\nWould you like to restart?"),
|
||||
pluginName));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
92
LanMountainDesktop/ViewModels/PrivacyPolicyViewModel.cs
Normal file
92
LanMountainDesktop/ViewModels/PrivacyPolicyViewModel.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.ViewModels;
|
||||
|
||||
public sealed partial class PrivacyPolicyViewModel : ViewModelBase
|
||||
{
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly string _languageCode;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _title = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _description = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _loadingText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _errorText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _markdownContent = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isLoading = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _hasError;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _hasContent;
|
||||
|
||||
public PrivacyPolicyViewModel()
|
||||
{
|
||||
_languageCode = "zh-CN";
|
||||
RefreshLocalizedText();
|
||||
LoadPrivacyPolicy();
|
||||
}
|
||||
|
||||
private void RefreshLocalizedText()
|
||||
{
|
||||
Title = L("settings.privacy.policy_title", "Privacy Policy");
|
||||
Description = L("settings.privacy.policy_description", "Learn how we collect, use, and protect your data.");
|
||||
LoadingText = L("settings.privacy.policy_loading", "Loading privacy policy...");
|
||||
}
|
||||
|
||||
private async void LoadPrivacyPolicy()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
HasError = false;
|
||||
HasContent = false;
|
||||
|
||||
// 从嵌入资源加载隐私政策Markdown文件
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = "LanMountainDesktop.Assets.Documents.Privacy.md";
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream is null)
|
||||
{
|
||||
throw new FileNotFoundException($"Privacy policy resource not found: {resourceName}");
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
var markdown = await reader.ReadToEndAsync();
|
||||
|
||||
MarkdownContent = markdown;
|
||||
IsLoading = false;
|
||||
HasContent = true;
|
||||
|
||||
AppLogger.Info("PrivacyPolicy", "Privacy policy loaded successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("PrivacyPolicy", "Failed to load privacy policy.", ex);
|
||||
IsLoading = false;
|
||||
HasError = true;
|
||||
ErrorText = L("settings.privacy.policy_error", "Failed to load privacy policy. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
private string L(string key, string fallback)
|
||||
=> _localizationService.GetString(_languageCode, key, fallback);
|
||||
}
|
||||
@@ -15,6 +15,8 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
|
||||
private readonly string _languageCode;
|
||||
private bool _isInitializing;
|
||||
|
||||
public event Action? ViewPrivacyPolicyRequested;
|
||||
|
||||
public PrivacySettingsPageViewModel(ISettingsFacadeService settingsFacade)
|
||||
{
|
||||
_settingsFacade = settingsFacade;
|
||||
@@ -59,6 +61,12 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
|
||||
[ObservableProperty]
|
||||
private string _refreshDeviceIdText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _viewPrivacyPolicyText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _privacyPolicyHintPrefix = string.Empty;
|
||||
|
||||
public void Load()
|
||||
{
|
||||
var state = _settingsFacade.Privacy.Get();
|
||||
@@ -130,6 +138,25 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
|
||||
DeviceIdHeader = L("settings.privacy.device_id_title", "Device ID");
|
||||
DeviceIdDescription = L("settings.privacy.device_id_description", "Unique identifier for this device. Click refresh to regenerate.");
|
||||
RefreshDeviceIdText = L("settings.privacy.refresh_device_id", "Refresh");
|
||||
PrivacyPolicyHintPrefix = L("settings.privacy.policy_hint_prefix", "For more details, please ");
|
||||
ViewPrivacyPolicyText = L("settings.privacy.view_policy", "view our privacy policy");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ViewPrivacyPolicy()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 触发隐私政策查看事件
|
||||
AppLogger.Info("PrivacySettings", "User requested to view privacy policy.");
|
||||
|
||||
// 发送事件通知显示隐私政策
|
||||
ViewPrivacyPolicyRequested?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("PrivacySettings", "Failed to view privacy policy.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private string L(string key, string fallback)
|
||||
|
||||
@@ -268,12 +268,17 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase
|
||||
|
||||
partial void OnSelectedLanguageChanged(SelectionOption value)
|
||||
{
|
||||
RefreshPreview();
|
||||
if (_isInitializing || value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新语言代码并刷新UI文本
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(value.Value);
|
||||
RefreshLocalizedText();
|
||||
RefreshPreview();
|
||||
|
||||
// 保存设置
|
||||
_settingsFacade.Region.Save(new RegionSettingsState(
|
||||
value.Value,
|
||||
NormalizeTimeZoneId(SelectedTimeZone?.Id)));
|
||||
@@ -1329,9 +1334,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
|
||||
LoadStateFromSettings();
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _autoCheckUpdates;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _selectedUpdateChannelValue = UpdateSettingsValues.ChannelStable;
|
||||
|
||||
@@ -1380,9 +1382,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
|
||||
[ObservableProperty]
|
||||
private string _preferencesDescription = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _autoCheckUpdatesLabel = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _updateChannelLabel = string.Empty;
|
||||
|
||||
@@ -1520,16 +1519,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
|
||||
|
||||
private bool IsBusy => IsCheckingForUpdates || IsDownloading;
|
||||
|
||||
partial void OnAutoCheckUpdatesChanged(bool value)
|
||||
{
|
||||
if (_isInitializing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveUpdateSettings();
|
||||
}
|
||||
|
||||
partial void OnSelectedUpdateChannelOptionChanged(SelectionOption? value)
|
||||
{
|
||||
if (value is not null &&
|
||||
@@ -1729,7 +1718,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
|
||||
var current = _settingsFacade.Update.Get();
|
||||
_settingsFacade.Update.Save(current with
|
||||
{
|
||||
AutoCheckUpdates = AutoCheckUpdates,
|
||||
IncludePrereleaseUpdates = string.Equals(
|
||||
SelectedUpdateChannelValue,
|
||||
UpdateSettingsValues.ChannelPreview,
|
||||
@@ -1841,7 +1829,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
|
||||
StatusCardDescription = L("settings.update.status_card_description", "Check for updates and review the latest release information.");
|
||||
PreferencesHeader = L("settings.update.preferences_header", "Update Preferences");
|
||||
PreferencesDescription = L("settings.update.preferences_description", "Choose your release channel, download source, behavior, and download speed.");
|
||||
AutoCheckUpdatesLabel = L("settings.update.auto_check_toggle", "Automatically check for updates on startup");
|
||||
UpdateChannelLabel = L("settings.update.channel_label", "Update Channel");
|
||||
UpdateSourceLabel = L("settings.update.source_label", "Download Source");
|
||||
UpdateModeLabel = L("settings.update.mode_label", "Update Mode");
|
||||
@@ -1870,7 +1857,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
|
||||
{
|
||||
var update = _settingsFacade.Update.Get();
|
||||
_isInitializing = true;
|
||||
AutoCheckUpdates = update.AutoCheckUpdates;
|
||||
SelectedUpdateChannelValue = UpdateSettingsValues.NormalizeChannel(update.UpdateChannel, update.IncludePrereleaseUpdates);
|
||||
SelectedUpdateSourceValue = UpdateSettingsValues.NormalizeDownloadSource(update.UpdateDownloadSource);
|
||||
SelectedUpdateModeValue = UpdateSettingsValues.NormalizeMode(update.UpdateMode);
|
||||
|
||||
@@ -82,23 +82,24 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
|
||||
[ObservableProperty]
|
||||
private bool _isImage;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isVideo;
|
||||
|
||||
[ObservableProperty]
|
||||
private Bitmap? _previewImage;
|
||||
|
||||
[ObservableProperty]
|
||||
private IBrush? _previewBrush;
|
||||
|
||||
// 自定义颜色持久化
|
||||
[ObservableProperty]
|
||||
private string _videoModeHintText = string.Empty;
|
||||
private Color _customColor = Colors.White;
|
||||
|
||||
[ObservableProperty]
|
||||
private IBrush _customColorBrush = new SolidColorBrush(Colors.White);
|
||||
|
||||
public void Load()
|
||||
{
|
||||
var wallpaper = _settingsFacade.Wallpaper.Get();
|
||||
WallpaperPath = wallpaper.WallpaperPath ?? string.Empty;
|
||||
|
||||
|
||||
SelectedWallpaperType = WallpaperTypes.FirstOrDefault(t => t.Value == wallpaper.Type) ?? WallpaperTypes[0];
|
||||
SelectedColor = wallpaper.Color ?? PresetColors[0];
|
||||
|
||||
@@ -108,7 +109,14 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
|
||||
SelectedWallpaperPlacement = WallpaperPlacements.FirstOrDefault(option =>
|
||||
string.Equals(option.Value, wallpaperPlacement, StringComparison.OrdinalIgnoreCase))
|
||||
?? WallpaperPlacements[0];
|
||||
|
||||
|
||||
// 加载自定义颜色
|
||||
if (!string.IsNullOrWhiteSpace(wallpaper.CustomColor) && Color.TryParse(wallpaper.CustomColor, out var customColor))
|
||||
{
|
||||
CustomColor = customColor;
|
||||
CustomColorBrush = new SolidColorBrush(customColor);
|
||||
}
|
||||
|
||||
UpdateVisibility();
|
||||
UpdatePreviewFromCurrentSelection();
|
||||
}
|
||||
@@ -124,8 +132,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
|
||||
private void UpdateVisibility()
|
||||
{
|
||||
IsImage = SelectedWallpaperType?.Value == "Image";
|
||||
IsVideo = SelectedWallpaperType?.Value == "Video";
|
||||
IsImageOrVideo = SelectedWallpaperType?.Value is "Image" or "Video";
|
||||
IsImageOrVideo = IsImage;
|
||||
IsSolidColor = SelectedWallpaperType?.Value == "SolidColor";
|
||||
}
|
||||
|
||||
@@ -135,6 +142,16 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
|
||||
SaveWallpaper();
|
||||
}
|
||||
|
||||
partial void OnCustomColorChanged(Color value)
|
||||
{
|
||||
CustomColorBrush = new SolidColorBrush(value);
|
||||
// 将自定义颜色应用到壁纸
|
||||
var colorHex = $"#{value.A:X2}{value.R:X2}{value.G:X2}{value.B:X2}";
|
||||
SelectedColor = colorHex;
|
||||
if (_isInitializing) return;
|
||||
SaveWallpaper();
|
||||
}
|
||||
|
||||
public async Task ImportWallpaperAsync(string sourcePath)
|
||||
{
|
||||
var importedPath = await _settingsFacade.WallpaperMedia.ImportAssetAsync(sourcePath);
|
||||
@@ -222,11 +239,13 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
|
||||
var normalizedPath = SelectedWallpaperType?.Value == "SolidColor" || string.IsNullOrWhiteSpace(WallpaperPath)
|
||||
? null
|
||||
: WallpaperPath;
|
||||
var customColorHex = $"#{CustomColor.A:X2}{CustomColor.R:X2}{CustomColor.G:X2}{CustomColor.B:X2}";
|
||||
_settingsFacade.Wallpaper.Save(new WallpaperSettingsState(
|
||||
normalizedPath,
|
||||
selectedType,
|
||||
SelectedColor,
|
||||
selectedPlacement));
|
||||
selectedPlacement,
|
||||
customColorHex));
|
||||
}
|
||||
|
||||
private IReadOnlyList<SelectionOption> CreateWallpaperPlacements()
|
||||
@@ -246,7 +265,6 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
|
||||
return
|
||||
[
|
||||
new SelectionOption("Image", L("settings.wallpaper.type.image", "Image")),
|
||||
new SelectionOption("Video", L("settings.wallpaper.type.video", "Video")),
|
||||
new SelectionOption("SolidColor", L("settings.wallpaper.type.solid_color", "Solid Color"))
|
||||
];
|
||||
}
|
||||
@@ -257,7 +275,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
|
||||
[
|
||||
"#D8A7B1", "#B6C9BB", "#A2B5BB", "#E6E2D3",
|
||||
"#B5A397", "#C5C1C0", "#D4BE8D", "#C08261",
|
||||
"#8E9775", "#9FBAD3", "#E5BAA2", "#4E596F"
|
||||
"#8E9775", "#9FBAD3", "#E5BAA2"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -271,7 +289,6 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
|
||||
WallpaperPlacementDescription = L("settings.wallpaper.placement_desc", "Adjust how the image fills the desktop.");
|
||||
ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper");
|
||||
FilePickerTitle = L("filepicker.title", "Select wallpaper");
|
||||
VideoModeHintText = L("settings.wallpaper.video_mode", "Video wallpaper uses automatic fill mode.");
|
||||
}
|
||||
|
||||
private string L(string key, string fallback)
|
||||
|
||||
@@ -17,6 +17,22 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="ColorSchemeHeaderTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<StackPanel Spacing="8">
|
||||
<RadioButton x:Name="FollowSystemRadioButton"
|
||||
GroupName="ColorScheme"
|
||||
IsCheckedChanged="OnColorSchemeChanged" />
|
||||
<RadioButton x:Name="UseNativeRadioButton"
|
||||
GroupName="ColorScheme"
|
||||
IsCheckedChanged="OnColorSchemeChanged" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
@@ -11,13 +11,15 @@ using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
|
||||
{
|
||||
private readonly List<ImportedClassScheduleSnapshot> _importedSchedules = [];
|
||||
private string _activeScheduleId = string.Empty;
|
||||
private string? _activeScheduleId;
|
||||
private bool _suppressEvents;
|
||||
|
||||
public ClassScheduleComponentEditor()
|
||||
: this(null)
|
||||
@@ -62,10 +64,49 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
|
||||
|
||||
private void ApplyState()
|
||||
{
|
||||
var snapshot = LoadSnapshot();
|
||||
var colorSchemeSource = snapshot.ColorSchemeSource;
|
||||
|
||||
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Class Schedule";
|
||||
DescriptionTextBlock.Text = L("schedule.settings.desc", "导入 ClassIsland 的 CSES 课表文件并选择启用项。");
|
||||
|
||||
ColorSchemeHeaderTextBlock.Text = L("component.settings.color_scheme", "配色方案");
|
||||
FollowSystemRadioButton.Content = L("component.color_scheme.follow_system", "跟随系统配色");
|
||||
UseNativeRadioButton.Content = L("component.color_scheme.native", "使用组件自定义配色");
|
||||
|
||||
AddScheduleButton.Content = L("schedule.settings.add", "添加课表");
|
||||
EmptyStateTextBlock.Text = L("schedule.settings.empty", "暂无导入课表");
|
||||
|
||||
_suppressEvents = true;
|
||||
|
||||
if (string.IsNullOrEmpty(colorSchemeSource) ||
|
||||
colorSchemeSource == ThemeAppearanceValues.ColorSchemeFollowSystem)
|
||||
{
|
||||
FollowSystemRadioButton.IsChecked = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
UseNativeRadioButton.IsChecked = true;
|
||||
}
|
||||
|
||||
_suppressEvents = false;
|
||||
}
|
||||
|
||||
private void OnColorSchemeChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_suppressEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var useNative = UseNativeRadioButton.IsChecked == true;
|
||||
var colorSchemeSource = useNative
|
||||
? ThemeAppearanceValues.ColorSchemeNative
|
||||
: ThemeAppearanceValues.ColorSchemeFollowSystem;
|
||||
|
||||
var snapshot = LoadSnapshot();
|
||||
snapshot.ColorSchemeSource = colorSchemeSource;
|
||||
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ColorSchemeSource));
|
||||
}
|
||||
|
||||
private async void OnAddScheduleClick(object? sender, RoutedEventArgs e)
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Views.ComponentEditors.StudyEnvironmentComponentEditor">
|
||||
<StackPanel Spacing="16">
|
||||
<Border Classes="component-editor-hero-card"
|
||||
<Border Classes="component-editor-hero_card"
|
||||
Padding="24">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock x:Name="HeadlineTextBlock"
|
||||
@@ -17,6 +18,22 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="ColorSchemeHeaderTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<StackPanel Spacing="8">
|
||||
<RadioButton x:Name="FollowSystemRadioButton"
|
||||
GroupName="ColorScheme"
|
||||
IsCheckedChanged="OnColorSchemeChanged" />
|
||||
<RadioButton x:Name="UseNativeRadioButton"
|
||||
GroupName="ColorScheme"
|
||||
IsCheckedChanged="OnColorSchemeChanged" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
@@ -27,7 +44,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
<Border Classes="component-editor_card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="DbfsHeaderTextBlock"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Avalonia.Interactivity;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
@@ -25,6 +26,8 @@ public partial class StudyEnvironmentComponentEditor : ComponentEditorViewBase
|
||||
var snapshot = LoadSnapshot();
|
||||
var showDisplayDb = snapshot.StudyEnvironmentShowDisplayDb;
|
||||
var showDbfs = snapshot.StudyEnvironmentShowDbfs;
|
||||
var colorSchemeSource = snapshot.ColorSchemeSource;
|
||||
|
||||
if (!showDisplayDb && !showDbfs)
|
||||
{
|
||||
showDisplayDb = true;
|
||||
@@ -32,16 +35,49 @@ public partial class StudyEnvironmentComponentEditor : ComponentEditorViewBase
|
||||
|
||||
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Study Environment";
|
||||
DescriptionTextBlock.Text = L("study.environment.settings.desc", "配置右侧实时噪音值显示内容。");
|
||||
|
||||
ColorSchemeHeaderTextBlock.Text = L("component.settings.color_scheme", "配色方案");
|
||||
FollowSystemRadioButton.Content = L("component.color_scheme.follow_system", "跟随系统配色");
|
||||
UseNativeRadioButton.Content = L("component.color_scheme.native", "使用组件自定义配色");
|
||||
|
||||
DisplayDbToggleSwitch.Content = L("study.environment.settings.show_display_db", "显示 display dB");
|
||||
DbfsToggleSwitch.Content = L("study.environment.settings.show_dbfs", "显示 dBFS");
|
||||
HintTextBlock.Text = L("study.environment.settings.hint", "至少启用一种显示方式。");
|
||||
|
||||
_suppressEvents = true;
|
||||
|
||||
if (string.IsNullOrEmpty(colorSchemeSource) ||
|
||||
colorSchemeSource == ThemeAppearanceValues.ColorSchemeFollowSystem)
|
||||
{
|
||||
FollowSystemRadioButton.IsChecked = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
UseNativeRadioButton.IsChecked = true;
|
||||
}
|
||||
|
||||
DisplayDbToggleSwitch.IsChecked = showDisplayDb;
|
||||
DbfsToggleSwitch.IsChecked = showDbfs;
|
||||
_suppressEvents = false;
|
||||
}
|
||||
|
||||
private void OnColorSchemeChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_suppressEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var useNative = UseNativeRadioButton.IsChecked == true;
|
||||
var colorSchemeSource = useNative
|
||||
? ThemeAppearanceValues.ColorSchemeNative
|
||||
: ThemeAppearanceValues.ColorSchemeFollowSystem;
|
||||
|
||||
var snapshot = LoadSnapshot();
|
||||
snapshot.ColorSchemeSource = colorSchemeSource;
|
||||
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ColorSchemeSource));
|
||||
}
|
||||
|
||||
private void OnToggleChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
|
||||
@@ -13,6 +13,7 @@ using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
@@ -50,6 +51,7 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
||||
private bool _autoRefreshEnabled = true;
|
||||
private string _sourceType = BaiduHotSearchSourceTypes.Official;
|
||||
private bool _isNightVisual = true;
|
||||
private string? _componentColorScheme;
|
||||
|
||||
private sealed record HotItemVisual(
|
||||
Border Host,
|
||||
@@ -180,17 +182,25 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
||||
|
||||
private void ApplyNightModeVisual()
|
||||
{
|
||||
var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor(
|
||||
_componentColorScheme,
|
||||
ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode());
|
||||
|
||||
var brandColor = useMonetColor
|
||||
? (_isNightVisual ? Color.Parse("#9FABFF") : Color.Parse("#4F6BEB"))
|
||||
: (_isNightVisual ? Color.Parse("#5D93FF") : Color.Parse("#2932E1"));
|
||||
|
||||
CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD"));
|
||||
RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#00000000"));
|
||||
|
||||
BrandTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#5D93FF") : Color.Parse("#2932E1"));
|
||||
BrandTextBlock.Foreground = new SolidColorBrush(brandColor);
|
||||
|
||||
RefreshButton.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#EFF1F5"));
|
||||
RefreshGlyphIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671"));
|
||||
|
||||
foreach (var visual in _hotItemVisuals)
|
||||
{
|
||||
visual.IndexTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#5D93FF") : Color.Parse("#2932E1"));
|
||||
visual.IndexTextBlock.Foreground = new SolidColorBrush(brandColor);
|
||||
visual.TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327"));
|
||||
}
|
||||
|
||||
@@ -488,10 +498,11 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
||||
enabled = snapshot.BaiduHotSearchAutoRefreshEnabled;
|
||||
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.BaiduHotSearchAutoRefreshIntervalMinutes);
|
||||
sourceType = BaiduHotSearchSourceTypes.Normalize(snapshot.BaiduHotSearchSourceType);
|
||||
_componentColorScheme = snapshot.ColorSchemeSource;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep fallback defaults.
|
||||
_componentColorScheme = null;
|
||||
}
|
||||
|
||||
_autoRefreshEnabled = enabled;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<Grid Grid.Row="1">
|
||||
<ScrollViewer x:Name="ContentScrollViewer"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Disabled">
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="CourseListPanel" />
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
@@ -25,9 +26,17 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
private readonly DispatcherTimer _refreshTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMinutes(4)
|
||||
Interval = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
|
||||
private int _lastCurrentCourseIndex = -1;
|
||||
private DateOnly _lastRefreshDate = DateOnly.MinValue;
|
||||
|
||||
private bool _isUserScrolling;
|
||||
private Vector _lastScrollOffset;
|
||||
private Point _dragStartPoint;
|
||||
private Point _lastDragPoint;
|
||||
|
||||
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService();
|
||||
@@ -39,6 +48,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
private string _languageCode = "zh-CN";
|
||||
private string _componentId = BuiltInComponentIds.DesktopClassSchedule;
|
||||
private string _placementId = string.Empty;
|
||||
private string? _componentColorScheme;
|
||||
|
||||
public ClassScheduleWidget()
|
||||
{
|
||||
@@ -50,6 +60,10 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ContentScrollViewer.PointerPressed += OnScrollViewerPointerPressed;
|
||||
ContentScrollViewer.PointerMoved += OnScrollViewerPointerMoved;
|
||||
ContentScrollViewer.PointerReleased += OnScrollViewerPointerReleased;
|
||||
|
||||
ApplyCellSize(_currentCellSize);
|
||||
RefreshSchedule();
|
||||
}
|
||||
@@ -107,9 +121,109 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
RefreshSchedule();
|
||||
}
|
||||
|
||||
private void OnScrollViewerPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
_isUserScrolling = true;
|
||||
_dragStartPoint = e.GetCurrentPoint(ContentScrollViewer).Position;
|
||||
_lastDragPoint = _dragStartPoint;
|
||||
_lastScrollOffset = ContentScrollViewer.Offset;
|
||||
}
|
||||
|
||||
private void OnScrollViewerPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (!_isUserScrolling)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentPoint = e.GetCurrentPoint(ContentScrollViewer);
|
||||
var currentPosition = currentPoint.Position;
|
||||
var deltaY = currentPosition.Y - _lastDragPoint.Y;
|
||||
|
||||
var newOffset = _lastScrollOffset;
|
||||
newOffset = newOffset.WithY(newOffset.Y - deltaY);
|
||||
|
||||
ContentScrollViewer.Offset = newOffset;
|
||||
_lastDragPoint = currentPosition;
|
||||
}
|
||||
|
||||
private void OnScrollViewerPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
_lastScrollOffset = ContentScrollViewer.Offset;
|
||||
}
|
||||
|
||||
private void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||
var currentDate = DateOnly.FromDateTime(now);
|
||||
|
||||
var previousCourseIndex = _lastCurrentCourseIndex;
|
||||
|
||||
RefreshSchedule();
|
||||
|
||||
var newCurrentCourseIndex = FindCurrentCourseIndex();
|
||||
_lastCurrentCourseIndex = newCurrentCourseIndex;
|
||||
|
||||
if (previousCourseIndex != newCurrentCourseIndex && newCurrentCourseIndex >= 0)
|
||||
{
|
||||
if (_isUserScrolling)
|
||||
{
|
||||
_isUserScrolling = false;
|
||||
}
|
||||
ScrollToCurrentCourse(newCurrentCourseIndex);
|
||||
}
|
||||
|
||||
if (_lastRefreshDate != currentDate && currentDate > _lastRefreshDate)
|
||||
{
|
||||
_lastRefreshDate = currentDate;
|
||||
}
|
||||
}
|
||||
|
||||
private int FindCurrentCourseIndex()
|
||||
{
|
||||
for (var i = 0; i < _courseItems.Count; i++)
|
||||
{
|
||||
if (_courseItems[i].IsCurrent)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void ScrollToCurrentCourse(int courseIndex)
|
||||
{
|
||||
if (courseIndex < 0 || courseIndex >= _courseItems.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保在UI线程执行
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (courseIndex >= CourseListPanel.Children.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var targetChild = CourseListPanel.Children[courseIndex];
|
||||
if (targetChild == null || !targetChild.IsArrangeValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bounds = targetChild.Bounds;
|
||||
var scrollViewerHeight = ContentScrollViewer.Bounds.Height;
|
||||
var contentHeight = CourseListPanel.Bounds.Height;
|
||||
|
||||
// 计算滚动位置,使当前课程居中显示
|
||||
var targetOffset = bounds.Position.Y - (scrollViewerHeight / 2) + (bounds.Height / 2);
|
||||
|
||||
// 确保不超出边界
|
||||
targetOffset = Math.Max(0, Math.Min(targetOffset, contentHeight - scrollViewerHeight));
|
||||
|
||||
ContentScrollViewer.Offset = new Vector(0, targetOffset);
|
||||
}, Avalonia.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
public void RefreshFromSettings()
|
||||
@@ -134,45 +248,85 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
_componentId,
|
||||
_placementId);
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode);
|
||||
_componentColorScheme = componentSettings.ColorSchemeSource;
|
||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||
UpdateHeader(now);
|
||||
var today = DateOnly.FromDateTime(now);
|
||||
|
||||
var importedSchedulePath = ResolveImportedSchedulePath(componentSettings);
|
||||
var readResult = _scheduleService.Load(importedSchedulePath);
|
||||
if (!readResult.Success || readResult.Snapshot is null)
|
||||
{
|
||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
||||
UpdateHeader(now);
|
||||
ShowStatus(L("schedule.widget.no_source", "未读取到 ClassIsland 课表"));
|
||||
RenderScheduleItems();
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshot = readResult.Snapshot;
|
||||
var today = DateOnly.FromDateTime(now);
|
||||
|
||||
if (!_scheduleService.TryResolveClassPlanForDate(snapshot, today, out var resolvedClassPlan))
|
||||
{
|
||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
||||
ShowStatus(L("schedule.widget.no_class_today", "今天没有课程"));
|
||||
RenderScheduleItems();
|
||||
return;
|
||||
var nextDay = today.AddDays(1);
|
||||
if (_scheduleService.TryResolveClassPlanForDate(snapshot, nextDay, out var nextDayClassPlan))
|
||||
{
|
||||
resolvedClassPlan = nextDayClassPlan;
|
||||
today = nextDay;
|
||||
}
|
||||
else
|
||||
{
|
||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
||||
UpdateHeader(now);
|
||||
ShowStatus(L("schedule.widget.no_class_today", "今天没有课程"));
|
||||
RenderScheduleItems();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!snapshot.TimeLayouts.TryGetValue(resolvedClassPlan.ClassPlan.TimeLayoutId, out var layout))
|
||||
{
|
||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
||||
UpdateHeader(now);
|
||||
ShowStatus(L("schedule.widget.layout_missing", "课表时间布局缺失"));
|
||||
RenderScheduleItems();
|
||||
return;
|
||||
}
|
||||
|
||||
_courseItems = BuildCourseItemViewModels(snapshot, resolvedClassPlan.ClassPlan, layout, now);
|
||||
var adjustedNow = today == DateOnly.FromDateTime(now) ? now : DateTime.Today.AddHours(8);
|
||||
_courseItems = BuildCourseItemViewModels(snapshot, resolvedClassPlan.ClassPlan, layout, adjustedNow);
|
||||
|
||||
if (_courseItems.Count == 0)
|
||||
{
|
||||
var nextDay = today.AddDays(1);
|
||||
if (_scheduleService.TryResolveClassPlanForDate(snapshot, nextDay, out var nextDayClassPlan) &&
|
||||
snapshot.TimeLayouts.TryGetValue(nextDayClassPlan.ClassPlan.TimeLayoutId, out var nextLayout))
|
||||
{
|
||||
today = nextDay;
|
||||
adjustedNow = DateTime.Today.AddHours(8);
|
||||
_courseItems = BuildCourseItemViewModels(snapshot, nextDayClassPlan.ClassPlan, nextLayout, adjustedNow);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateHeader(today.ToDateTime(TimeOnly.MinValue));
|
||||
|
||||
if (_courseItems.Count == 0)
|
||||
{
|
||||
ShowStatus(L("schedule.widget.no_class_today", "今天没有课程"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentIndex = FindCurrentCourseIndex();
|
||||
_lastCurrentCourseIndex = currentIndex;
|
||||
HideStatus();
|
||||
|
||||
// 初始化时自动跳转到当前课程
|
||||
if (currentIndex >= 0)
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
ScrollToCurrentCourse(currentIndex);
|
||||
}, Avalonia.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
}
|
||||
|
||||
RenderScheduleItems();
|
||||
@@ -336,6 +490,10 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
return;
|
||||
}
|
||||
|
||||
var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor(
|
||||
_componentColorScheme,
|
||||
ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode());
|
||||
|
||||
var scale = ResolveScale();
|
||||
var bulletSize = Math.Clamp(10 * scale, 5, 12);
|
||||
var courseNameSize = Math.Clamp(42 * scale, 14, 42);
|
||||
@@ -350,13 +508,14 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
var primaryBrush = CreateBrush(_isNightVisual ? "#F9FBFF" : "#151821");
|
||||
var secondaryBrush = CreateBrush(_isNightVisual ? "#848B99" : "#667084");
|
||||
var currentBrush = CreateBrush("#FF4D5A");
|
||||
var currentBrush = useMonetColor
|
||||
? CreateBrush("#FF4FC3F7")
|
||||
: CreateBrush("#FF4D5A");
|
||||
var normalBulletBrush = CreateBrush(_isNightVisual ? "#B8BEC9" : "#9AA3B2");
|
||||
|
||||
var visibleItems = _courseItems.Take(maxVisibleItems).ToList();
|
||||
for (var i = 0; i < visibleItems.Count; i++)
|
||||
for (var i = 0; i < _courseItems.Count; i++)
|
||||
{
|
||||
var item = visibleItems[i];
|
||||
var item = _courseItems[i];
|
||||
var bulletBrush = item.IsCurrent ? currentBrush : normalBulletBrush;
|
||||
|
||||
var bullet = new Border
|
||||
@@ -438,9 +597,22 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
private void ApplyAdaptiveLayout()
|
||||
{
|
||||
if (Bounds.Width <= 0 || Bounds.Height <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scale = ResolveScale();
|
||||
_isNightVisual = ResolveNightMode();
|
||||
|
||||
var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor(
|
||||
_componentColorScheme,
|
||||
ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode());
|
||||
|
||||
var slashBrush = useMonetColor
|
||||
? CreateBrush("#FF4FC3F7")
|
||||
: CreateBrush("#FF3250");
|
||||
|
||||
var cornerRadius = Math.Clamp(_currentCellSize * 0.45, 24, 44);
|
||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
||||
RootBorder.Background = _isNightVisual
|
||||
@@ -468,7 +640,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
MonthTextBlock.Foreground = CreateBrush(_isNightVisual ? "#F8FAFF" : "#131722");
|
||||
DayTextBlock.Foreground = CreateBrush(_isNightVisual ? "#F8FAFF" : "#131722");
|
||||
SlashTextBlock.Foreground = CreateBrush("#FF3250");
|
||||
SlashTextBlock.Foreground = slashBrush;
|
||||
WeekdayTextBlock.Foreground = CreateBrush(_isNightVisual ? "#C6CBD5" : "#4B5463");
|
||||
ClassCountTextBlock.Foreground = CreateBrush(_isNightVisual ? "#8D95A4" : "#738095");
|
||||
StatusTextBlock.Foreground = CreateBrush(_isNightVisual ? "#9AA2B1" : "#4B5565");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
@@ -439,6 +439,11 @@ public sealed class DesktopComponentRuntimeRegistry
|
||||
"component.browser",
|
||||
() => new BrowserWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.24, 10, 24)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopOfficeRecentDocuments,
|
||||
"component.office_recent_documents",
|
||||
() => new OfficeRecentDocumentsWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.50, 10, 24)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.HolidayCalendar,
|
||||
"component.holiday_calendar",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public sealed class OfficeRecentDocumentViewModel
|
||||
{
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public string TimeAgo { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:vm="using:LanMountainDesktop.Views.Components"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="640"
|
||||
d:DesignHeight="320"
|
||||
x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="34"
|
||||
Background="#2D5A8E"
|
||||
ClipToBounds="True"
|
||||
BorderThickness="0"
|
||||
Padding="0">
|
||||
<Grid>
|
||||
<Border x:Name="AccentCorner"
|
||||
Width="140"
|
||||
Height="140"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,-40,-40,0"
|
||||
CornerRadius="70"
|
||||
Background="#4A90D9"
|
||||
Opacity="0.3"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<Grid RowDefinitions="Auto,*" RowSpacing="8" Margin="16,14,16,14">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="HeaderTextBlock"
|
||||
Text="最近文档"
|
||||
Foreground="#D8FFFFFF"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
<Button x:Name="RefreshButton"
|
||||
Grid.Column="1"
|
||||
Width="28"
|
||||
Height="28"
|
||||
CornerRadius="14"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
Focusable="False"
|
||||
PointerPressed="OnRefreshPointerPressed">
|
||||
<fi:SymbolIcon Symbol="ArrowSync"
|
||||
FontSize="14"
|
||||
Foreground="#B8FFFFFF" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Disabled"
|
||||
Margin="0,4,0,0">
|
||||
<ItemsControl x:Name="DocumentsItemsControl">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:OfficeRecentDocumentViewModel">
|
||||
<Border x:Name="DocumentCard"
|
||||
Width="130"
|
||||
Height="90"
|
||||
CornerRadius="10"
|
||||
Background="#3AFFFFFF"
|
||||
Padding="10"
|
||||
Cursor="Hand"
|
||||
PointerPressed="OnDocumentCardPointerPressed">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="{Binding FileName}"
|
||||
Foreground="#D8FFFFFF"
|
||||
FontSize="12"
|
||||
FontWeight="Medium"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="2"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock Grid.Row="2"
|
||||
Text="{Binding TimeAgo}"
|
||||
Foreground="#9AFFFFFF"
|
||||
FontSize="10"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="StatusTextBlock"
|
||||
IsVisible="False"
|
||||
Text="暂无最近文档"
|
||||
Foreground="#9AFFFFFF"
|
||||
FontSize="14"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget
|
||||
{
|
||||
private readonly IOfficeRecentDocumentsService _recentDocumentsService;
|
||||
private List<OfficeRecentDocument> _documents = new();
|
||||
private bool _isOnActivePage;
|
||||
private bool _isEditMode;
|
||||
private bool _isLoading;
|
||||
|
||||
public OfficeRecentDocumentsWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
_recentDocumentsService = new OfficeRecentDocumentsService();
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
if (RootBorder is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scale = cellSize / 100.0;
|
||||
RootBorder.CornerRadius = new Avalonia.CornerRadius(Math.Max(8, 34 * scale));
|
||||
}
|
||||
|
||||
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||
{
|
||||
_isOnActivePage = isOnActivePage;
|
||||
_isEditMode = isEditMode;
|
||||
|
||||
if (_isOnActivePage && !_isLoading)
|
||||
{
|
||||
LoadDocuments();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadDocuments()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isLoading = true;
|
||||
StatusTextBlock.IsVisible = false;
|
||||
|
||||
_documents = _recentDocumentsService.GetRecentDocuments(20);
|
||||
|
||||
if (_documents.Count == 0)
|
||||
{
|
||||
StatusTextBlock.Text = "暂无最近文档";
|
||||
StatusTextBlock.IsVisible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateDisplay();
|
||||
}
|
||||
catch
|
||||
{
|
||||
StatusTextBlock.Text = "加载失败";
|
||||
StatusTextBlock.IsVisible = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDisplay()
|
||||
{
|
||||
var displayItems = _documents.Select(d => new OfficeRecentDocumentViewModel
|
||||
{
|
||||
FileName = d.FileName,
|
||||
FilePath = d.FilePath,
|
||||
TimeAgo = GetTimeAgo(d.LastModifiedTime)
|
||||
}).ToList();
|
||||
|
||||
DocumentsItemsControl.ItemsSource = displayItems;
|
||||
}
|
||||
|
||||
private static string GetTimeAgo(DateTime dateTime)
|
||||
{
|
||||
var span = DateTime.Now - dateTime;
|
||||
|
||||
if (span.TotalMinutes < 1)
|
||||
return "刚刚";
|
||||
if (span.TotalMinutes < 60)
|
||||
return $"{(int)span.TotalMinutes} 分钟前";
|
||||
if (span.TotalHours < 24)
|
||||
return $"{(int)span.TotalHours} 小时前";
|
||||
if (span.TotalDays < 7)
|
||||
return $"{(int)span.TotalDays} 天前";
|
||||
if (span.TotalDays < 30)
|
||||
return $"{(int)(span.TotalDays / 7)} 周前";
|
||||
|
||||
return dateTime.ToString("MM/dd");
|
||||
}
|
||||
|
||||
private void OnRefreshPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
LoadDocuments();
|
||||
}
|
||||
|
||||
private void OnDocumentCardPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (sender is Border border && border.DataContext is { } data)
|
||||
{
|
||||
var filePathProperty = data.GetType().GetProperty("FilePath");
|
||||
var filePath = filePathProperty?.GetValue(data) as string;
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
_recentDocumentsService.OpenDocument(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
@@ -24,6 +25,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
||||
private double _currentCellSize = 48;
|
||||
private bool _showDisplayDb = true;
|
||||
private bool _showDbfs;
|
||||
private string? _componentColorScheme;
|
||||
private string _languageCode = "zh-CN";
|
||||
private bool _isAttached;
|
||||
private bool _isOnActivePage = true;
|
||||
@@ -147,6 +149,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
|
||||
_showDisplayDb = componentSnapshot.StudyEnvironmentShowDisplayDb;
|
||||
_showDbfs = componentSnapshot.StudyEnvironmentShowDbfs;
|
||||
_componentColorScheme = componentSnapshot.ColorSchemeSource;
|
||||
if (!_showDisplayDb && !_showDbfs)
|
||||
{
|
||||
_showDisplayDb = true;
|
||||
@@ -287,22 +290,26 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
||||
|
||||
private IBrush ResolveStatusBrush(StudyAnalyticsSnapshot snapshot)
|
||||
{
|
||||
var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor(
|
||||
_componentColorScheme,
|
||||
ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode());
|
||||
|
||||
if (snapshot.State == StudyAnalyticsRuntimeState.Unsupported ||
|
||||
snapshot.State == StudyAnalyticsRuntimeState.Error ||
|
||||
snapshot.StreamStatus == NoiseStreamStatus.Error)
|
||||
{
|
||||
return CreateBrush("#FFFF7B7B");
|
||||
return useMonetColor ? CreateBrush("#FF6FD7A2") : CreateBrush("#FFFF7B7B");
|
||||
}
|
||||
|
||||
if (snapshot.StreamStatus == NoiseStreamStatus.Noisy)
|
||||
{
|
||||
return CreateBrush("#FFFFB14A");
|
||||
return useMonetColor ? CreateBrush("#FF4FC3F7") : CreateBrush("#FFFFB14A");
|
||||
}
|
||||
|
||||
if (snapshot.State == StudyAnalyticsRuntimeState.Running &&
|
||||
snapshot.StreamStatus == NoiseStreamStatus.Quiet)
|
||||
{
|
||||
return CreateBrush("#FF6FD7A2");
|
||||
return useMonetColor ? CreateBrush("#FF81C784") : CreateBrush("#FF6FD7A2");
|
||||
}
|
||||
|
||||
return TryResolveThemeBrush("AdaptiveTextPrimaryBrush", "#FFEFF3FF");
|
||||
|
||||
@@ -964,6 +964,7 @@ public partial class MainWindow
|
||||
DisposeComponentIfNeeded(host);
|
||||
contentHost.Child = component;
|
||||
ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen);
|
||||
InvalidateDesktopPageAwareComponentContextCache();
|
||||
UpdateDesktopPageAwareComponentContext();
|
||||
if (_selectedDesktopComponentHost == host)
|
||||
{
|
||||
@@ -1102,6 +1103,7 @@ public partial class MainWindow
|
||||
|
||||
ClearTimeZoneServiceBindings(pageGrid.Children.OfType<Control>().ToList());
|
||||
pageGrid.Children.Clear();
|
||||
InvalidateDesktopPageAwareComponentContextCache();
|
||||
|
||||
var maxColumns = pageGrid.ColumnDefinitions.Count;
|
||||
var maxRows = pageGrid.RowDefinitions.Count;
|
||||
@@ -1204,6 +1206,7 @@ public partial class MainWindow
|
||||
pageGrid.Children.Add(host);
|
||||
|
||||
_desktopComponentPlacements.Add(placement);
|
||||
InvalidateDesktopPageAwareComponentContextCache();
|
||||
UpdateDesktopPageAwareComponentContext();
|
||||
PersistSettings();
|
||||
|
||||
@@ -1577,14 +1580,86 @@ public partial class MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidateDesktopPageAwareComponentContextCache()
|
||||
{
|
||||
_desktopPageContextInitialized = false;
|
||||
_desktopPageContextActiveMask = 0;
|
||||
}
|
||||
|
||||
private int BuildDesktopPageAwareComponentActiveMask()
|
||||
{
|
||||
if (_isSettingsOpen)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var activeMask = 0;
|
||||
if (_desktopSurfacePageWidth > 1 &&
|
||||
_desktopPagesHostTransform is not null &&
|
||||
(_isDesktopSwipeActive ||
|
||||
_desktopPageContextSettlingSourceIndex is not null ||
|
||||
_desktopPageContextSettlingTargetIndex is not null))
|
||||
{
|
||||
var viewportLeft = -_desktopPagesHostTransform.X;
|
||||
var viewportRight = viewportLeft + _desktopSurfacePageWidth;
|
||||
for (var pageIndex = 0; pageIndex < _desktopPageCount; pageIndex++)
|
||||
{
|
||||
var pageLeft = pageIndex * _desktopSurfacePageWidth;
|
||||
var pageRight = pageLeft + _desktopSurfacePageWidth;
|
||||
if (pageRight > viewportLeft + 0.5d && pageLeft < viewportRight - 0.5d)
|
||||
{
|
||||
activeMask |= 1 << pageIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentDesktopSurfaceIndex >= 0 && _currentDesktopSurfaceIndex < _desktopPageCount)
|
||||
{
|
||||
activeMask |= 1 << _currentDesktopSurfaceIndex;
|
||||
}
|
||||
|
||||
if (_desktopPageContextSettlingSourceIndex is int sourceIndex &&
|
||||
sourceIndex >= 0 &&
|
||||
sourceIndex < _desktopPageCount)
|
||||
{
|
||||
activeMask |= 1 << sourceIndex;
|
||||
}
|
||||
|
||||
if (_desktopPageContextSettlingTargetIndex is int targetIndex &&
|
||||
targetIndex >= 0 &&
|
||||
targetIndex < _desktopPageCount)
|
||||
{
|
||||
activeMask |= 1 << targetIndex;
|
||||
}
|
||||
|
||||
return activeMask;
|
||||
}
|
||||
|
||||
private void UpdateDesktopPageAwareComponentContext()
|
||||
{
|
||||
var activeDesktopPageIndex = _isSettingsOpen ? -1 : _currentDesktopSurfaceIndex;
|
||||
var isEditMode = _isComponentLibraryOpen || _isSettingsOpen;
|
||||
var activeMask = BuildDesktopPageAwareComponentActiveMask();
|
||||
var pageUpdateMask = !_desktopPageContextInitialized || isEditMode != _desktopPageContextEditMode
|
||||
? _desktopPageComponentGrids.Keys.Aggregate(0, (mask, pageIndex) => mask | (1 << pageIndex))
|
||||
: activeMask ^ _desktopPageContextActiveMask;
|
||||
|
||||
if (_desktopPageContextInitialized &&
|
||||
pageUpdateMask == 0 &&
|
||||
isEditMode == _desktopPageContextEditMode &&
|
||||
activeMask == _desktopPageContextActiveMask)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var pair in _desktopPageComponentGrids)
|
||||
{
|
||||
var isOnActivePage = pair.Key == activeDesktopPageIndex;
|
||||
var pageBit = 1 << pair.Key;
|
||||
if ((pageUpdateMask & pageBit) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var isOnActivePage = (activeMask & pageBit) != 0;
|
||||
foreach (var host in pair.Value.Children.OfType<Border>())
|
||||
{
|
||||
if (!host.Classes.Contains(DesktopComponentHostClass))
|
||||
@@ -1598,6 +1673,10 @@ public partial class MainWindow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_desktopPageContextInitialized = true;
|
||||
_desktopPageContextEditMode = isEditMode;
|
||||
_desktopPageContextActiveMask = activeMask;
|
||||
}
|
||||
|
||||
private static void ApplyDesktopPageContext(Control root, bool isOnActivePage, bool isEditMode)
|
||||
@@ -2702,6 +2781,11 @@ public partial class MainWindow
|
||||
return Symbol.Apps;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Folder;
|
||||
}
|
||||
|
||||
return Symbol.Apps;
|
||||
}
|
||||
|
||||
@@ -2747,6 +2831,11 @@ public partial class MainWindow
|
||||
return L("component_category.study", "Study");
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return L("component_category.file", "File");
|
||||
}
|
||||
|
||||
return categoryId;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
@@ -16,6 +17,7 @@ using Avalonia.VisualTree;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Theme;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
@@ -54,6 +56,8 @@ public partial class MainWindow
|
||||
private int _currentDesktopSurfaceIndex;
|
||||
private double _desktopSurfacePageWidth;
|
||||
private TranslateTransform? _desktopPagesHostTransform;
|
||||
private Transitions? _desktopPagesHostSnapTransitions;
|
||||
private bool _desktopPagesHostTransitionsSuspended;
|
||||
private bool _isDesktopSwipeActive;
|
||||
private bool _isDesktopSwipeDirectionLocked;
|
||||
private Point _desktopSwipeStartPoint;
|
||||
@@ -62,6 +66,12 @@ public partial class MainWindow
|
||||
private long _desktopSwipeLastTimestamp;
|
||||
private double _desktopSwipeVelocityX;
|
||||
private double _desktopSwipeBaseOffset;
|
||||
private bool _desktopPageContextInitialized;
|
||||
private bool _desktopPageContextEditMode;
|
||||
private int _desktopPageContextActiveMask;
|
||||
private int? _desktopPageContextSettlingSourceIndex;
|
||||
private int? _desktopPageContextSettlingTargetIndex;
|
||||
private int _desktopPageContextSettleRevision;
|
||||
|
||||
private int LauncherSurfaceIndex => Math.Max(MinDesktopPageCount, _desktopPageCount);
|
||||
|
||||
@@ -164,6 +174,15 @@ public partial class MainWindow
|
||||
DesktopPagesHost.RenderTransform = _desktopPagesHostTransform;
|
||||
}
|
||||
|
||||
if (_desktopPagesHostTransitionsSuspended)
|
||||
{
|
||||
_desktopPagesHostTransform.Transitions = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_desktopPagesHostSnapTransitions ??= _desktopPagesHostTransform.Transitions;
|
||||
}
|
||||
|
||||
var viewportRow = gridMetrics.RowCount > 2 ? 1 : 0;
|
||||
var viewportRowSpan = gridMetrics.RowCount > 2 ? gridMetrics.RowCount - 2 : 1;
|
||||
var pageWidth = Math.Max(1, gridMetrics.GridWidthPx);
|
||||
@@ -200,6 +219,7 @@ public partial class MainWindow
|
||||
DesktopPagesContainer.Width = pageWidth * _desktopPageCount;
|
||||
DesktopPagesContainer.Height = pageHeight;
|
||||
_desktopPageComponentGrids.Clear();
|
||||
InvalidateDesktopPageAwareComponentContextCache();
|
||||
for (var index = 0; index < _desktopPageCount; index++)
|
||||
{
|
||||
DesktopPagesContainer.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(pageWidth, GridUnitType.Pixel)));
|
||||
@@ -354,6 +374,88 @@ public partial class MainWindow
|
||||
UpdateDesktopPageAwareComponentContext();
|
||||
}
|
||||
|
||||
private void SetDesktopPagesHostSnapAnimationEnabled(bool enabled)
|
||||
{
|
||||
if (_desktopPagesHostTransform is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
if (!_desktopPagesHostTransitionsSuspended)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_desktopPagesHostTransform.Transitions = _desktopPagesHostSnapTransitions;
|
||||
_desktopPagesHostTransitionsSuspended = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_desktopPagesHostTransitionsSuspended)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_desktopPagesHostSnapTransitions ??= _desktopPagesHostTransform.Transitions;
|
||||
_desktopPagesHostTransform.Transitions = null;
|
||||
_desktopPagesHostTransitionsSuspended = true;
|
||||
}
|
||||
|
||||
private void ClearDesktopPageContextSettle(bool refreshContext)
|
||||
{
|
||||
_desktopPageContextSettleRevision++;
|
||||
_desktopPageContextSettlingSourceIndex = null;
|
||||
_desktopPageContextSettlingTargetIndex = null;
|
||||
|
||||
if (refreshContext)
|
||||
{
|
||||
UpdateDesktopPageAwareComponentContext();
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginDesktopPageContextSettle(int previousIndex, int targetIndex)
|
||||
{
|
||||
var sourceIndex = previousIndex >= 0 && previousIndex < _desktopPageCount
|
||||
? previousIndex
|
||||
: (int?)null;
|
||||
var destinationIndex = targetIndex >= 0 && targetIndex < _desktopPageCount
|
||||
? targetIndex
|
||||
: (int?)null;
|
||||
|
||||
if (sourceIndex == destinationIndex && destinationIndex is not null)
|
||||
{
|
||||
ClearDesktopPageContextSettle(refreshContext: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceIndex is null && destinationIndex is null)
|
||||
{
|
||||
ClearDesktopPageContextSettle(refreshContext: false);
|
||||
return;
|
||||
}
|
||||
|
||||
_desktopPageContextSettleRevision++;
|
||||
var settleRevision = _desktopPageContextSettleRevision;
|
||||
_desktopPageContextSettlingSourceIndex = sourceIndex;
|
||||
_desktopPageContextSettlingTargetIndex = destinationIndex;
|
||||
|
||||
DispatcherTimer.RunOnce(
|
||||
() =>
|
||||
{
|
||||
if (settleRevision != _desktopPageContextSettleRevision)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_desktopPageContextSettlingSourceIndex = null;
|
||||
_desktopPageContextSettlingTargetIndex = null;
|
||||
UpdateDesktopPageAwareComponentContext();
|
||||
},
|
||||
FluttermotionToken.Page + TimeSpan.FromMilliseconds(36));
|
||||
}
|
||||
|
||||
private void MoveSurfaceBy(int delta)
|
||||
{
|
||||
if (delta == 0)
|
||||
@@ -373,9 +475,11 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
var previousIndex = _currentDesktopSurfaceIndex;
|
||||
_currentDesktopSurfaceIndex = target;
|
||||
BeginDesktopPageContextSettle(previousIndex, target);
|
||||
ApplyDesktopSurfaceOffset();
|
||||
PersistSettings();
|
||||
SchedulePersistSettings(delayMs: Math.Max(280, (int)FluttermotionToken.Page.TotalMilliseconds + 80));
|
||||
}
|
||||
|
||||
private bool CanSwipeDesktopSurface()
|
||||
@@ -426,6 +530,7 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
ClearDesktopPageContextSettle(refreshContext: false);
|
||||
_isDesktopSwipeActive = true;
|
||||
_isDesktopSwipeDirectionLocked = false;
|
||||
_desktopSwipeStartPoint = pointerInViewport;
|
||||
@@ -603,6 +708,7 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
_isDesktopSwipeDirectionLocked = true;
|
||||
SetDesktopPagesHostSnapAnimationEnabled(enabled: false);
|
||||
if (e.Pointer.Captured != DesktopPagesViewport)
|
||||
{
|
||||
e.Pointer.Capture(DesktopPagesViewport);
|
||||
@@ -621,6 +727,7 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
_desktopPagesHostTransform.X = tentative;
|
||||
UpdateDesktopPageAwareComponentContext();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
@@ -656,6 +763,7 @@ public partial class MainWindow
|
||||
_desktopSwipeLastTimestamp = 0;
|
||||
if (wasDirectionLocked)
|
||||
{
|
||||
SetDesktopPagesHostSnapAnimationEnabled(enabled: true);
|
||||
ApplyDesktopSurfaceOffset();
|
||||
}
|
||||
}
|
||||
@@ -682,6 +790,8 @@ public partial class MainWindow
|
||||
return false;
|
||||
}
|
||||
|
||||
SetDesktopPagesHostSnapAnimationEnabled(enabled: true);
|
||||
|
||||
var deltaX = _desktopSwipeCurrentPoint.X - _desktopSwipeStartPoint.X;
|
||||
var deltaY = _desktopSwipeCurrentPoint.Y - _desktopSwipeStartPoint.Y;
|
||||
var absDeltaX = Math.Abs(deltaX);
|
||||
|
||||
@@ -16,8 +16,6 @@ using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Theme;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
using LibVLCSharp.Shared;
|
||||
using LibVLCSharp.Avalonia;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
@@ -34,6 +32,11 @@ public partial class MainWindow
|
||||
{
|
||||
_ = sender;
|
||||
|
||||
if (_suppressOwnSettingsReloadCount > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Scope == SettingsScope.App && e.ChangedKeys is { Count: > 0 })
|
||||
{
|
||||
var changedKeys = e.ChangedKeys.ToArray();
|
||||
@@ -213,7 +216,6 @@ public partial class MainWindow
|
||||
_wallpaperType = string.IsNullOrWhiteSpace(type) ? "Image" : type.Trim();
|
||||
_wallpaperPlacement = WallpaperImageBrushFactory.NormalizePlacement(placement);
|
||||
_wallpaperSolidColor = TryParseColor(color, out var parsedColor) ? parsedColor : null;
|
||||
_wallpaperVideoPath = null;
|
||||
_wallpaperDisplayState = WallpaperDisplayState.NoWallpaperConfigured;
|
||||
|
||||
_wallpaperBitmap?.Dispose();
|
||||
@@ -235,17 +237,6 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(_wallpaperPath);
|
||||
var requestedTypeIsVideo = string.Equals(_wallpaperType, "Video", StringComparison.OrdinalIgnoreCase);
|
||||
if (SupportedVideoExtensions.Contains(extension) || requestedTypeIsVideo)
|
||||
{
|
||||
_wallpaperMediaType = WallpaperMediaType.Video;
|
||||
_wallpaperVideoPath = _wallpaperPath;
|
||||
_wallpaperDisplayState = File.Exists(_wallpaperPath)
|
||||
? WallpaperDisplayState.CurrentValidWallpaper
|
||||
: WallpaperDisplayState.TemporarilyUnavailable;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SupportedImageExtensions.Contains(extension))
|
||||
{
|
||||
_wallpaperMediaType = WallpaperMediaType.Image;
|
||||
@@ -285,7 +276,6 @@ public partial class MainWindow
|
||||
if (_wallpaperMediaType == WallpaperMediaType.SolidColor && _wallpaperSolidColor.HasValue)
|
||||
{
|
||||
DesktopWallpaperLayer.Background = new SolidColorBrush(_wallpaperSolidColor.Value);
|
||||
ApplyVideoWallpaperPosterVisibility(showPoster: false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -296,7 +286,6 @@ public partial class MainWindow
|
||||
DesktopWallpaperLayer.Background = _defaultDesktopBackground ?? CreateNeutralWallpaperFallbackBrush();
|
||||
DesktopWallpaperImageLayer.Background = WallpaperImageBrushFactory.Create(_wallpaperBitmap, _wallpaperPlacement);
|
||||
DesktopWallpaperImageLayer.IsVisible = true;
|
||||
ApplyVideoWallpaperPosterVisibility(showPoster: false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -308,92 +297,17 @@ public partial class MainWindow
|
||||
DesktopWallpaperLayer.Background = _defaultDesktopBackground ?? CreateNeutralWallpaperFallbackBrush();
|
||||
DesktopWallpaperImageLayer.Background = WallpaperImageBrushFactory.Create(_lastValidWallpaperBitmap, _wallpaperPlacement);
|
||||
DesktopWallpaperImageLayer.IsVisible = true;
|
||||
ApplyVideoWallpaperPosterVisibility(showPoster: false);
|
||||
return;
|
||||
}
|
||||
|
||||
DesktopWallpaperLayer.Background = _defaultDesktopBackground ?? CreateNeutralWallpaperFallbackBrush();
|
||||
ApplyVideoWallpaperPosterVisibility(
|
||||
showPoster: _wallpaperMediaType == WallpaperMediaType.Video && _videoWallpaperPosterBitmap is not null);
|
||||
}
|
||||
|
||||
private void UpdateWallpaperDisplay()
|
||||
{
|
||||
if (_wallpaperMediaType == WallpaperMediaType.Video)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_wallpaperVideoPath))
|
||||
{
|
||||
StartVideoWallpaper(_wallpaperVideoPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StopVideoWallpaper();
|
||||
}
|
||||
|
||||
ApplyWallpaperBrush();
|
||||
}
|
||||
|
||||
private void StartVideoWallpaper(string videoPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(videoPath) || !File.Exists(videoPath))
|
||||
{
|
||||
ApplyVideoWallpaperPosterVisibility(showPoster: _videoWallpaperPosterBitmap is not null);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_libVlc ??= new LibVLC();
|
||||
_videoWallpaperPlayer ??= new MediaPlayer(_libVlc);
|
||||
|
||||
if (_videoWallpaperMedia?.Mrl != videoPath)
|
||||
{
|
||||
_videoWallpaperMedia?.Dispose();
|
||||
_videoWallpaperMedia = new Media(_libVlc, new Uri(videoPath));
|
||||
_videoWallpaperPlayer.Media = _videoWallpaperMedia;
|
||||
}
|
||||
|
||||
if (DesktopVideoWallpaperView is { } videoView)
|
||||
{
|
||||
videoView.MediaPlayer = _videoWallpaperPlayer;
|
||||
videoView.IsVisible = true;
|
||||
}
|
||||
|
||||
if (!string.Equals(_videoWallpaperPosterPath, videoPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ApplyVideoWallpaperPosterVisibility(showPoster: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyVideoWallpaperPosterVisibility(showPoster: _videoWallpaperPosterBitmap is not null);
|
||||
}
|
||||
|
||||
if (!_videoWallpaperPlayer.IsPlaying)
|
||||
{
|
||||
_videoWallpaperPlayer.Play();
|
||||
}
|
||||
|
||||
TryCaptureVideoWallpaperPosterFrame(videoPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ApplyVideoWallpaperPosterVisibility(showPoster: _videoWallpaperPosterBitmap is not null);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopVideoWallpaper()
|
||||
{
|
||||
if (DesktopVideoWallpaperView is { } videoView)
|
||||
{
|
||||
videoView.IsVisible = false;
|
||||
}
|
||||
|
||||
_videoWallpaperPlayer?.Stop();
|
||||
_wallpaperVideoPath = null;
|
||||
ApplyVideoWallpaperPosterVisibility(showPoster: false);
|
||||
}
|
||||
|
||||
private double CalculateCurrentBackgroundLuminance()
|
||||
{
|
||||
var brush = DesktopWallpaperLayer.Background;
|
||||
@@ -473,6 +387,7 @@ public partial class MainWindow
|
||||
|
||||
private void PersistSettings()
|
||||
{
|
||||
_persistSettingsRevision++;
|
||||
if (_suppressSettingsPersistence)
|
||||
{
|
||||
return;
|
||||
@@ -480,6 +395,8 @@ public partial class MainWindow
|
||||
|
||||
try
|
||||
{
|
||||
// Saving our own state should not trigger a full external reload cycle.
|
||||
_suppressOwnSettingsReloadCount++;
|
||||
_settingsService.SaveSnapshot(SettingsScope.App, BuildAppSettingsSnapshot());
|
||||
_componentLayoutStore.SaveLayout(BuildDesktopLayoutSettingsSnapshot());
|
||||
_settingsService.SaveSnapshot(SettingsScope.Launcher, BuildLauncherSettingsSnapshot());
|
||||
@@ -488,11 +405,29 @@ public partial class MainWindow
|
||||
{
|
||||
AppLogger.Warn("SettingsRuntime", "Failed to persist settings.", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_suppressOwnSettingsReloadCount > 0)
|
||||
{
|
||||
_suppressOwnSettingsReloadCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SchedulePersistSettings(int delayMs = 200)
|
||||
{
|
||||
DispatcherTimer.RunOnce(PersistSettings, TimeSpan.FromMilliseconds(Math.Max(0, delayMs)));
|
||||
var revision = ++_persistSettingsRevision;
|
||||
DispatcherTimer.RunOnce(
|
||||
() =>
|
||||
{
|
||||
if (revision != _persistSettingsRevision)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PersistSettings();
|
||||
},
|
||||
TimeSpan.FromMilliseconds(Math.Max(0, delayMs)));
|
||||
}
|
||||
|
||||
internal void ReloadFromPersistedSettings()
|
||||
@@ -564,6 +499,7 @@ public partial class MainWindow
|
||||
var latestWeatherState = _weatherSettingsService.Get();
|
||||
var latestUpdateState = _updateSettingsService.Get();
|
||||
var latestThemeState = _themeSettingsService.Get();
|
||||
var latestPrivacyState = _settingsFacade.Privacy.Get();
|
||||
return new AppSettingsSnapshot
|
||||
{
|
||||
GridShortSideCells = _targetShortSideCells,
|
||||
@@ -595,7 +531,8 @@ public partial class MainWindow
|
||||
WeatherNoTlsRequests = latestWeatherState.NoTlsRequests,
|
||||
AutoStartWithWindows = _autoStartWithWindows,
|
||||
AppRenderMode = _selectedAppRenderMode,
|
||||
AutoCheckUpdates = latestUpdateState.AutoCheckUpdates,
|
||||
UploadAnonymousCrashData = latestPrivacyState.UploadAnonymousCrashData,
|
||||
UploadAnonymousUsageData = latestPrivacyState.UploadAnonymousUsageData,
|
||||
IncludePrereleaseUpdates = latestUpdateState.IncludePrereleaseUpdates,
|
||||
UpdateChannel = latestUpdateState.UpdateChannel,
|
||||
UpdateMode = latestUpdateState.UpdateMode,
|
||||
@@ -644,112 +581,6 @@ public partial class MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyVideoWallpaperPosterVisibility(bool showPoster)
|
||||
{
|
||||
if (DesktopVideoWallpaperImage is not { } posterImage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showPoster ||
|
||||
_videoWallpaperPosterBitmap is null ||
|
||||
!string.Equals(_videoWallpaperPosterPath, _wallpaperVideoPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
posterImage.IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
posterImage.Source = _videoWallpaperPosterBitmap;
|
||||
posterImage.IsVisible = true;
|
||||
}
|
||||
|
||||
private void TryCaptureVideoWallpaperPosterFrame(string videoPath)
|
||||
{
|
||||
if (_videoWallpaperPlayer is null || string.IsNullOrWhiteSpace(videoPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var snapshotPath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"lanmountaindesktop-wallpaper-poster-{Guid.NewGuid():N}.png");
|
||||
|
||||
try
|
||||
{
|
||||
for (var attempt = 0; attempt < 12; attempt++)
|
||||
{
|
||||
await Task.Delay(250).ConfigureAwait(false);
|
||||
|
||||
if (_wallpaperMediaType != WallpaperMediaType.Video ||
|
||||
!string.Equals(_wallpaperVideoPath, videoPath, StringComparison.OrdinalIgnoreCase) ||
|
||||
_videoWallpaperPlayer is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_videoWallpaperPlayer.TakeSnapshot(0, snapshotPath, 640, 360))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!File.Exists(snapshotPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(snapshotPath);
|
||||
if (fileInfo.Length <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Bitmap posterBitmap;
|
||||
await using (var stream = File.OpenRead(snapshotPath))
|
||||
{
|
||||
posterBitmap = new Bitmap(stream);
|
||||
}
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (_wallpaperMediaType != WallpaperMediaType.Video ||
|
||||
!string.Equals(_wallpaperVideoPath, videoPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
posterBitmap.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
_videoWallpaperPosterBitmap?.Dispose();
|
||||
_videoWallpaperPosterBitmap = posterBitmap;
|
||||
_videoWallpaperPosterPath = videoPath;
|
||||
ApplyVideoWallpaperPosterVisibility(showPoster: true);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort poster capture only.
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(snapshotPath))
|
||||
{
|
||||
File.Delete(snapshotPath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort cleanup only.
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private DesktopLayoutSettingsSnapshot BuildDesktopLayoutSettingsSnapshot()
|
||||
{
|
||||
return new DesktopLayoutSettingsSnapshot
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
xmlns:ic="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:mi="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:comp="using:LanMountainDesktop.Views.Components"
|
||||
xmlns:vlc="clr-namespace:LibVLCSharp.Avalonia;assembly=LibVLCSharp.Avalonia"
|
||||
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
@@ -123,18 +123,7 @@
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent" />
|
||||
|
||||
<Image x:Name="DesktopVideoWallpaperImage"
|
||||
IsVisible="False"
|
||||
IsHitTestVisible="False"
|
||||
Stretch="UniformToFill"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" />
|
||||
|
||||
<vlc:VideoView x:Name="DesktopVideoWallpaperView"
|
||||
IsVisible="False"
|
||||
IsHitTestVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" />
|
||||
|
||||
<Grid x:Name="DesktopGrid"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -156,7 +145,9 @@
|
||||
<TranslateTransform>
|
||||
<TranslateTransform.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="X" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
|
||||
<DoubleTransition Property="X"
|
||||
Duration="{StaticResource FluttermotionToken.Duration.Page}"
|
||||
Easing="0.22,1,0.36,1" />
|
||||
</Transitions>
|
||||
</TranslateTransform.Transitions>
|
||||
</TranslateTransform>
|
||||
|
||||
@@ -25,7 +25,7 @@ using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Theme;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
using LibVLCSharp.Shared;
|
||||
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
@@ -35,7 +35,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
{
|
||||
None,
|
||||
Image,
|
||||
Video,
|
||||
SolidColor
|
||||
}
|
||||
|
||||
@@ -65,10 +64,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
{
|
||||
".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp"
|
||||
};
|
||||
private static readonly HashSet<string> SupportedVideoExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v"
|
||||
};
|
||||
private static readonly TaskbarActionId[] DefaultPinnedTaskbarActions =
|
||||
[
|
||||
TaskbarActionId.MinimizeToWindows
|
||||
@@ -120,30 +115,11 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
private Bitmap? _wallpaperBitmap;
|
||||
private Bitmap? _lastValidWallpaperBitmap;
|
||||
private string? _lastValidWallpaperPath;
|
||||
private Bitmap? _videoWallpaperPosterBitmap;
|
||||
private string? _videoWallpaperPosterPath;
|
||||
private WallpaperMediaType _wallpaperMediaType;
|
||||
private WallpaperDisplayState _wallpaperDisplayState = WallpaperDisplayState.NoWallpaperConfigured;
|
||||
private string _wallpaperPlacement = WallpaperImageBrushFactory.Fill;
|
||||
private string? _wallpaperVideoPath;
|
||||
private string _wallpaperType = "Image";
|
||||
private Color? _wallpaperSolidColor;
|
||||
private LibVLC? _libVlc;
|
||||
private MediaPlayer? _videoWallpaperPlayer;
|
||||
private Media? _videoWallpaperMedia;
|
||||
private readonly object _desktopVideoFrameSync = new();
|
||||
private MediaPlayer.LibVLCVideoLockCb? _desktopVideoLockCallback;
|
||||
private MediaPlayer.LibVLCVideoUnlockCb? _desktopVideoUnlockCallback;
|
||||
private MediaPlayer.LibVLCVideoDisplayCb? _desktopVideoDisplayCallback;
|
||||
private DispatcherTimer? _desktopVideoFrameRefreshTimer;
|
||||
private IntPtr _desktopVideoFrameBufferPtr;
|
||||
private byte[]? _desktopVideoStagingBuffer;
|
||||
private WriteableBitmap? _desktopVideoBitmap;
|
||||
private int _desktopVideoFrameWidth;
|
||||
private int _desktopVideoFrameHeight;
|
||||
private int _desktopVideoFramePitch;
|
||||
private int _desktopVideoFrameBufferSize;
|
||||
private int _desktopVideoFrameDirtyFlag;
|
||||
private string? _wallpaperPath;
|
||||
private string _wallpaperStatus = "Current background uses solid color.";
|
||||
private IReadOnlyList<Color> _recommendedColors = Array.Empty<Color>();
|
||||
@@ -177,6 +153,8 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
private bool _isWeatherPreviewInProgress;
|
||||
private ClockDisplayFormat _clockDisplayFormat = ClockDisplayFormat.HourMinuteSecond;
|
||||
private bool _externalSettingsReloadPending;
|
||||
private int _persistSettingsRevision;
|
||||
private int _suppressOwnSettingsReloadCount;
|
||||
private double CurrentDesktopPitch => _currentDesktopCellSize + _currentDesktopCellGap;
|
||||
|
||||
public MainWindow()
|
||||
@@ -333,21 +311,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
_detachedComponentLibraryWindow.Close();
|
||||
}
|
||||
_detachedComponentLibraryWindow = null;
|
||||
StopVideoWallpaper();
|
||||
DisposeLauncherResources();
|
||||
_videoWallpaperMedia?.Dispose();
|
||||
_videoWallpaperMedia = null;
|
||||
_videoWallpaperPlayer?.Dispose();
|
||||
_videoWallpaperPlayer = null;
|
||||
_desktopVideoFrameRefreshTimer?.Stop();
|
||||
_desktopVideoFrameRefreshTimer = null;
|
||||
_videoWallpaperPosterBitmap?.Dispose();
|
||||
_videoWallpaperPosterBitmap = null;
|
||||
_videoWallpaperPosterPath = null;
|
||||
_lastValidWallpaperBitmap?.Dispose();
|
||||
_lastValidWallpaperBitmap = null;
|
||||
_libVlc?.Dispose();
|
||||
_libVlc = null;
|
||||
if (_recommendationInfoService is IDisposable recommendationServiceDisposable)
|
||||
{
|
||||
recommendationServiceDisposable.Dispose();
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="12">
|
||||
<TextBox Text="{Binding SearchText}"
|
||||
Watermark="{Binding SearchPlaceholder}" />
|
||||
<TextBox x:Name="SearchTextBox"
|
||||
Text="{Binding SearchText}"
|
||||
Watermark="{Binding SearchPlaceholder}"
|
||||
Focusable="True"
|
||||
IsTabStop="True" />
|
||||
<Button Grid.Column="1"
|
||||
Command="{Binding RefreshCommand}"
|
||||
Content="{Binding RefreshButtonText}" />
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia"
|
||||
xmlns:helpers="using:LanMountainDesktop.Helpers"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.PrivacyPolicyDrawer"
|
||||
x:DataType="vm:PrivacyPolicyViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container"
|
||||
Margin="0,0,0,8">
|
||||
<Border Classes="settings-section-card">
|
||||
<StackPanel Spacing="12">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="12">
|
||||
<Border Classes="settings-section-card-icon-host"
|
||||
Width="48"
|
||||
Height="48">
|
||||
<Viewbox Stretch="Uniform">
|
||||
<fi:SymbolIcon Symbol="Document" />
|
||||
</Viewbox>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="4"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Classes="settings-card-header"
|
||||
Margin="0"
|
||||
Text="{Binding Title}" />
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding Description}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="settings-section-card">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Classes="settings-item-description"
|
||||
IsVisible="{Binding IsLoading}"
|
||||
Text="{Binding LoadingText}" />
|
||||
|
||||
<TextBlock Classes="settings-item-description"
|
||||
IsVisible="{Binding HasError}"
|
||||
Text="{Binding ErrorText}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<mdxaml:MarkdownScrollViewer IsVisible="{Binding HasContent}"
|
||||
Markdown="{Binding MarkdownContent}"
|
||||
Engine="{x:Static helpers:PluginMarketMarkdownHelper.Engine}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,17 @@
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
public partial class PrivacyPolicyDrawer : UserControl
|
||||
{
|
||||
public PrivacyPolicyDrawer()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public PrivacyPolicyDrawer(PrivacyPolicyViewModel viewModel) : this()
|
||||
{
|
||||
DataContext = viewModel;
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,13 @@
|
||||
FontSize="12"
|
||||
Opacity="0.7"
|
||||
Margin="0,4,0,8" />
|
||||
<TextBox Text="{Binding DeviceId}"
|
||||
<TextBox x:Name="DeviceIdTextBox"
|
||||
Text="{Binding DeviceId}"
|
||||
IsReadOnly="True"
|
||||
FontFamily="Consolas"
|
||||
FontSize="12" />
|
||||
FontSize="12"
|
||||
Focusable="False"
|
||||
IsTabStop="False" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Content="{Binding RefreshDeviceIdText}"
|
||||
@@ -58,6 +61,28 @@
|
||||
Classes="accent-button" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Margin="0,16,0,0"
|
||||
Spacing="4">
|
||||
<TextBlock Text="{Binding PrivacyPolicyHintPrefix}"
|
||||
FontSize="13"
|
||||
VerticalAlignment="Center" />
|
||||
<Button Content="{Binding ViewPrivacyPolicyText}"
|
||||
Command="{Binding ViewPrivacyPolicyCommand}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource SystemAccentColor}"
|
||||
Cursor="Hand">
|
||||
<Button.Styles>
|
||||
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="TextBlock.Foreground" Value="{DynamicResource SystemAccentColorDark1}" />
|
||||
</Style>
|
||||
</Button.Styles>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -22,9 +22,17 @@ public partial class PrivacySettingsPage : SettingsPageBase
|
||||
public PrivacySettingsPage(PrivacySettingsPageViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
ViewModel.ViewPrivacyPolicyRequested += OnViewPrivacyPolicyRequested;
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public PrivacySettingsPageViewModel ViewModel { get; }
|
||||
|
||||
private void OnViewPrivacyPolicyRequested()
|
||||
{
|
||||
var privacyPolicyViewModel = new PrivacyPolicyViewModel();
|
||||
var drawer = new PrivacyPolicyDrawer(privacyPolicyViewModel);
|
||||
OpenDrawer(drawer, privacyPolicyViewModel.Title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,17 @@
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Opacity" Value="0.68" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="MaxWidth" Value="200" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.update-kv-value">
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="MaxWidth" Value="200" />
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
@@ -64,10 +69,12 @@
|
||||
Content="{Binding CheckForUpdatesButtonText}" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,*"
|
||||
ColumnSpacing="14"
|
||||
RowSpacing="12">
|
||||
<StackPanel Grid.Column="0"
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
RowDefinitions="Auto,Auto"
|
||||
ColumnSpacing="20"
|
||||
RowSpacing="16">
|
||||
<StackPanel Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Spacing="4">
|
||||
<TextBlock Classes="update-kv-label"
|
||||
Text="{Binding CurrentVersionLabel}" />
|
||||
@@ -75,7 +82,8 @@
|
||||
Text="{Binding CurrentVersionText}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
<StackPanel Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Spacing="4"
|
||||
IsVisible="{Binding IsLatestVersionVisible}">
|
||||
<TextBlock Classes="update-kv-label"
|
||||
@@ -105,18 +113,27 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
<StackPanel Spacing="12"
|
||||
HorizontalAlignment="Left">
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding UpdateStatus}" />
|
||||
Text="{Binding UpdateStatus}"
|
||||
TextWrapping="Wrap"
|
||||
HorizontalAlignment="Left"
|
||||
MaxWidth="500" />
|
||||
|
||||
<ProgressBar Minimum="0"
|
||||
Maximum="100"
|
||||
Value="{Binding DownloadProgressValue}"
|
||||
IsVisible="{Binding IsDownloadProgressVisible}" />
|
||||
IsVisible="{Binding IsDownloadProgressVisible}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="0,4,0,4" />
|
||||
|
||||
<TextBlock Classes="settings-item-description"
|
||||
IsVisible="{Binding IsDownloadProgressVisible}"
|
||||
Text="{Binding DownloadProgressText}" />
|
||||
Text="{Binding DownloadProgressText}"
|
||||
TextWrapping="Wrap"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="0,4,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
@@ -210,15 +227,7 @@
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding AutoCheckUpdatesLabel}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="ClockAlarm" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding AutoCheckUpdates}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -21,27 +21,11 @@
|
||||
CornerRadius="48"
|
||||
BoxShadow="0 12 32 #50000000">
|
||||
<Panel Background="{DynamicResource AdaptiveSurfaceBaseBrush}" Margin="2">
|
||||
<!-- 图片/视频预览 -->
|
||||
<!-- 图片预览 -->
|
||||
<Border Background="#FFF6F7F9"
|
||||
IsVisible="{Binding IsImage}">
|
||||
<Border Background="{Binding PreviewBrush}" />
|
||||
</Border>
|
||||
<Border Background="#FFF6F7F9"
|
||||
IsVisible="{Binding IsVideo}">
|
||||
<StackPanel HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="12">
|
||||
<fi:FluentIcon Icon="Video"
|
||||
Width="72"
|
||||
Height="72"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
<TextBlock Text="{Binding VideoModeHintText}"
|
||||
Width="300"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- 纯色预览 -->
|
||||
<Border Background="{Binding SelectedColor}"
|
||||
IsVisible="{Binding IsSolidColor}" />
|
||||
@@ -52,31 +36,104 @@
|
||||
|
||||
<!-- 右侧:颜色选择网格 -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="12" IsVisible="{Binding IsSolidColor}">
|
||||
<TextBlock Text="{Binding WallpaperColorLabel}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
<TextBlock Text="{Binding WallpaperColorLabel}"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Opacity="0.8" />
|
||||
<ItemsControl ItemsSource="{Binding PresetColors}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="4" Rows="3" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="{Binding}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding $parent[UserControl].((vm:WallpaperSettingsPageViewModel)DataContext).SelectColorCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
ToolTip.Tip="{Binding}">
|
||||
<!-- 简单的悬停与选中效果由 Button 默认样式提供,
|
||||
由于使用了低饱和度颜色,背景色本身就很柔和 -->
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<UniformGrid Columns="4" Rows="3">
|
||||
<!-- 预设颜色 1-11 -->
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="#D8A7B1"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding SelectColorCommand}"
|
||||
CommandParameter="#D8A7B1"
|
||||
ToolTip.Tip="#D8A7B1" />
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="#B6C9BB"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding SelectColorCommand}"
|
||||
CommandParameter="#B6C9BB"
|
||||
ToolTip.Tip="#B6C9BB" />
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="#A2B5BB"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding SelectColorCommand}"
|
||||
CommandParameter="#A2B5BB"
|
||||
ToolTip.Tip="#A2B5BB" />
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="#E6E2D3"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding SelectColorCommand}"
|
||||
CommandParameter="#E6E2D3"
|
||||
ToolTip.Tip="#E6E2D3" />
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="#B5A397"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding SelectColorCommand}"
|
||||
CommandParameter="#B5A397"
|
||||
ToolTip.Tip="#B5A397" />
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="#C5C1C0"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding SelectColorCommand}"
|
||||
CommandParameter="#C5C1C0"
|
||||
ToolTip.Tip="#C5C1C0" />
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="#D4BE8D"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding SelectColorCommand}"
|
||||
CommandParameter="#D4BE8D"
|
||||
ToolTip.Tip="#D4BE8D" />
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="#C08261"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding SelectColorCommand}"
|
||||
CommandParameter="#C08261"
|
||||
ToolTip.Tip="#C08261" />
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="#8E9775"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding SelectColorCommand}"
|
||||
CommandParameter="#8E9775"
|
||||
ToolTip.Tip="#8E9775" />
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="#9FBAD3"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding SelectColorCommand}"
|
||||
CommandParameter="#9FBAD3"
|
||||
ToolTip.Tip="#9FBAD3" />
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="#E5BAA2"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
Command="{Binding SelectColorCommand}"
|
||||
CommandParameter="#E5BAA2"
|
||||
ToolTip.Tip="#E5BAA2" />
|
||||
<!-- 第12个位置:自定义颜色选择器 -->
|
||||
<Button Width="48" Height="48" Margin="4" Padding="0"
|
||||
Background="{Binding CustomColorBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="6"
|
||||
ToolTip.Tip="自定义颜色">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="BottomEdgeAlignedLeft">
|
||||
<StackPanel Width="260" Spacing="12">
|
||||
<ColorPicker Color="{Binding CustomColor}" />
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -105,7 +162,7 @@
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<!-- 图片/视频文件选择 -->
|
||||
<!-- 图片文件选择 -->
|
||||
<ui:SettingsExpander Header="{Binding WallpaperPathLabel}"
|
||||
IsVisible="{Binding IsImage}"
|
||||
Margin="0,4,0,0">
|
||||
@@ -129,7 +186,7 @@
|
||||
<!-- 填充方式 -->
|
||||
<ui:SettingsExpander Header="{Binding WallpaperPlacementLabel}"
|
||||
Description="{Binding WallpaperPlacementDescription}"
|
||||
IsVisible="{Binding IsImageOrVideo}"
|
||||
IsVisible="{Binding IsImage}"
|
||||
Margin="0,4,0,0">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Maximize" />
|
||||
@@ -147,12 +204,6 @@
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<TextBlock Margin="0,8,0,0"
|
||||
IsVisible="{Binding IsVideo}"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="{Binding VideoModeHintText}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -96,8 +96,11 @@
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="12">
|
||||
<TextBox Text="{Binding SearchKeyword}"
|
||||
Watermark="{Binding SearchPlaceholder}" />
|
||||
<TextBox x:Name="SearchKeywordTextBox"
|
||||
Text="{Binding SearchKeyword}"
|
||||
Watermark="{Binding SearchPlaceholder}"
|
||||
Focusable="True"
|
||||
IsTabStop="True" />
|
||||
<Button Grid.Column="1"
|
||||
Command="{Binding SearchCommand}"
|
||||
Content="{Binding SearchButtonText}" />
|
||||
@@ -178,12 +181,18 @@
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem>
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBox Text="{Binding LocationKey}"
|
||||
Watermark="{Binding LocationKeyPlaceholder}" />
|
||||
<TextBox x:Name="LocationKeyTextBox"
|
||||
Text="{Binding LocationKey}"
|
||||
Watermark="{Binding LocationKeyPlaceholder}"
|
||||
Focusable="True"
|
||||
IsTabStop="True" />
|
||||
</ui:SettingsExpanderItem>
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBox Text="{Binding LocationName}"
|
||||
Watermark="{Binding LocationNamePlaceholder}" />
|
||||
<TextBox x:Name="LocationNameTextBox"
|
||||
Text="{Binding LocationName}"
|
||||
Watermark="{Binding LocationNamePlaceholder}"
|
||||
Focusable="True"
|
||||
IsTabStop="True" />
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
@@ -230,11 +239,14 @@
|
||||
<fi:SymbolIconSource Symbol="Warning" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<TextBox Width="360"
|
||||
<TextBox x:Name="ExcludedAlertsTextBox"
|
||||
Width="360"
|
||||
MinHeight="120"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding ExcludedAlerts}" />
|
||||
Text="{Binding ExcludedAlerts}"
|
||||
Focusable="True"
|
||||
IsTabStop="True" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user