Compare commits

...

2 Commits

Author SHA1 Message Date
lincube
298defb829 0.6.2
删除了视频壁纸功能,为纯色背景添加了自定义颜色选项。
2026-03-16 21:08:54 +08:00
lincube
bcf4be6d50 0.6.1
课表组件修复。加入最近文档组件。
2026-03-16 15:19:46 +08:00
36 changed files with 2505 additions and 461 deletions

View 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] 当前课程变化时自动复位到最新进行中课程

View 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
(无)

View 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 完成后进行

View File

@@ -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 控件残留

View File

@@ -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** 壁纸设置功能正常工作(仅支持图片和纯色)

View File

@@ -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
- 修改填充方式可见性绑定
### 验收标准
- [ ] 无视频预览 BorderIsVisible="{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

View File

@@ -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";
}

View File

@@ -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;
}
}
}

View File

@@ -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",

View File

@@ -52,15 +52,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')))&#xA; or '$(RuntimeIdentifier)' == 'win-x64'&#xA; or '$(RuntimeIdentifier)' == 'win-x86'" />
<PackageReference Include="VideoLAN.LibVLC.Mac" Version="3.1.3.1" Condition="('$(RuntimeIdentifier)' == '' and $([MSBuild]::IsOSPlatform('OSX')))&#xA; 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" />

View File

@@ -255,7 +255,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 +264,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",
@@ -580,6 +581,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",

View File

@@ -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": "选择顶部状态栏显示的组件。",
@@ -260,7 +254,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 +263,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 +387,6 @@
"settings.footer": "LanMountainDesktop 设置",
"filepicker.title": "选择壁纸",
"filepicker.image_files": "图片文件",
"filepicker.video_files": "视频文件",
"common.day": "日间",
"common.night": "夜间",
"common.back": "返回",
@@ -585,6 +579,7 @@
"component.whiteboard": "竖向小黑板",
"component.blackboard_landscape": "横向小黑板",
"component.browser": "浏览器",
"component.office_recent_documents": "最近文档",
"component.holiday_calendar": "节假日日历",
"component.study_environment": "环境",
"component.study_session_control": "自习时段控制",

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -0,0 +1,153 @@
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;
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>();
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;
}
var extension = Path.GetExtension(targetPath).ToLowerInvariant();
if (!IsOfficeFile(extension))
{
continue;
}
if (!System.IO.File.Exists(targetPath))
{
continue;
}
try
{
var fileInfo = new FileInfo(targetPath);
var doc = new OfficeRecentDocument
{
FileName = Path.GetFileNameWithoutExtension(targetPath),
FilePath = targetPath,
Extension = extension,
LastModifiedTime = fileInfo.LastWriteTime,
FileSizeBytes = fileInfo.Length,
IconGlyph = GetIconGlyph(extension)
};
if (!documents.Any(d => d.FilePath == targetPath))
{
documents.Add(doc);
}
}
catch
{
}
}
}
catch
{
}
}
return documents
.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
{
}
}
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"));
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);
}
}

View File

@@ -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,

View File

@@ -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;
}

View 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;
}
}
}

View File

@@ -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";

View File

@@ -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)

View File

@@ -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">

View File

@@ -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)

View File

@@ -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"

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,89 @@ 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;
}
if (courseIndex < CourseListPanel.Children.Count)
{
var targetChild = CourseListPanel.Children[courseIndex];
var bounds = targetChild.Bounds;
ContentScrollViewer.Offset = new Vector(0, bounds.Position.Y);
}
}
public void RefreshFromSettings()
@@ -134,44 +228,75 @@ 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();
}
@@ -336,6 +461,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,7 +479,9 @@ 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();
@@ -438,9 +569,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 +612,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");

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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="{DynamicResource CardBackgroundFillColorDefaultBrush}"
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="{DynamicResource SystemAccentColorLight2Brush}"
Opacity="0.2"
IsHitTestVisible="False" />
<Grid RowDefinitions="Auto,*" RowSpacing="8" Margin="16,14,16,14">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<TextBlock x:Name="HeaderTextBlock"
Text="最近文档"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
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="{DynamicResource AdaptiveTextSecondaryBrush}" />
</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="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
Padding="10"
Cursor="Hand"
PointerPressed="OnDocumentCardPointerPressed">
<Grid RowDefinitions="Auto,*,Auto">
<TextBlock Grid.Row="0"
Text="{Binding FileName}"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
FontSize="12"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"
MaxLines="2"
TextWrapping="Wrap"
VerticalAlignment="Top" />
<TextBlock Grid.Row="2"
Text="{Binding TimeAgo}"
Foreground="{DynamicResource AdaptiveTextTertiaryBrush}"
FontSize="10"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
<TextBlock x:Name="StatusTextBlock"
IsVisible="False"
Text="暂无最近文档"
Foreground="{DynamicResource AdaptiveTextTertiaryBrush}"
FontSize="14"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</Border>
</UserControl>

View File

@@ -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);
}
}
}
}

View File

@@ -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");

View File

@@ -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;
@@ -213,7 +211,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 +232,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 +271,6 @@ public partial class MainWindow
if (_wallpaperMediaType == WallpaperMediaType.SolidColor && _wallpaperSolidColor.HasValue)
{
DesktopWallpaperLayer.Background = new SolidColorBrush(_wallpaperSolidColor.Value);
ApplyVideoWallpaperPosterVisibility(showPoster: false);
return;
}
@@ -296,7 +281,6 @@ public partial class MainWindow
DesktopWallpaperLayer.Background = _defaultDesktopBackground ?? CreateNeutralWallpaperFallbackBrush();
DesktopWallpaperImageLayer.Background = WallpaperImageBrushFactory.Create(_wallpaperBitmap, _wallpaperPlacement);
DesktopWallpaperImageLayer.IsVisible = true;
ApplyVideoWallpaperPosterVisibility(showPoster: false);
return;
}
@@ -308,92 +292,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;
@@ -644,112 +553,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

View File

@@ -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"

View File

@@ -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>();
@@ -333,21 +309,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();

View File

@@ -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>