Compare commits

...

4 Commits

Author SHA1 Message Date
lincube
ac4617f5cf 0.6.4 2026-03-17 14:57:41 +08:00
lincube
0645598753 0.6.3.1
最近文件查看优化,课程表组件优化,插件安装优化。
2026-03-17 12:30:30 +08:00
lincube
dadd132b4f 0.6.3
优化了文本框焦点,优化了更新体验,优化了遥测,披露了收集的数据。
2026-03-17 01:01:48 +08:00
lincube
298defb829 0.6.2
删除了视频壁纸功能,为纯色背景添加了自定义颜色选项。
2026-03-16 21:08:54 +08:00
37 changed files with 2622 additions and 588 deletions

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

@@ -67,6 +67,15 @@ public partial class App : Application
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
(Current as App)?._hostApplicationLifecycle;
// 隐私政策查看事件
public static event Action? CurrentPrivacyPolicyViewRequested;
// 触发隐私政策查看事件的方法
public static void RaisePrivacyPolicyViewRequested()
{
CurrentPrivacyPolicyViewRequested?.Invoke();
}
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
public ISettingsFacadeService SettingsFacade => _settingsFacade;
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
@@ -501,6 +510,8 @@ public partial class App : Application
if (languageChanged)
{
// 清除本地化缓存,强制重新加载语言文件
_localizationService.ClearCache();
ApplyCurrentCultureFromSettings();
if (_trayIcons is not null)
{

View File

@@ -0,0 +1,326 @@
# LanMountainDesktop 隐私政策
**最后更新日期2026年3月17日**
---
## 引言
欢迎使用 LanMountainDesktop我们非常重视您的隐私保护。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的数据。
**请在使用本应用前仔细阅读本隐私政策。使用本应用即表示您同意本政策的条款。**
---
## 1. 数据收集范围
### 1.1 我们收集的数据
当您启用匿名数据收集功能时,我们会收集以下数据:
#### 匿名崩溃数据
- **崩溃报告**:应用崩溃时的错误日志和堆栈跟踪
- **设备信息**操作系统版本、设备型号、架构x64/x86
- **应用版本**:当前使用的应用版本号
- **设备标识符**匿名生成的唯一设备ID不包含个人信息
#### 匿名使用数据
- **应用启动和关闭事件**:记录应用何时启动和关闭
- **功能使用统计**:哪些功能被使用、使用频率
- **设置变更**:用户更改了哪些设置(不包含具体设置值)
- **界面交互**:点击了哪些按钮、访问了哪些页面
- **设备信息**:操作系统、应用版本、设备类型
### 1.2 始终收集的基础数据
**重要说明:** 为了统计应用的用户数量和日活跃用户,即使您关闭了匿名数据收集开关,我们仍会收集以下基础数据:
-**应用启动事件**:用于统计日活跃用户
-**设备标识符**:用于区分不同用户(不包含个人信息)
-**应用版本**:用于统计版本分布
**这些基础数据不包含任何个人身份信息,仅用于统计用户数量和应用使用情况。**
### 1.3 我们不收集的数据
我们**明确承诺不收集**以下数据:
- ❌ 个人身份信息(姓名、邮箱、电话等)
- ❌ 真实姓名或用户名
- ❌ 地理位置信息(精确位置)
- ❌ 文件内容或文档数据
- ❌ 密码或凭据信息
- ❌ 网络浏览历史
- ❌ 联系人信息
- ❌ 照片、视频或音频文件
---
## 2. 数据收集目的
我们收集数据的目的如下:
### 2.1 基础数据用途(始终收集)
- **统计用户数量**:了解应用的用户规模
- **统计日活跃用户**:了解应用的活跃程度
- **版本分布统计**:了解用户使用的版本情况
### 2.2 崩溃数据用途
- **提高应用稳定性**:识别和修复崩溃问题
- **优化性能**:分析性能瓶颈
- **改进用户体验**:了解应用在不同设备上的表现
### 2.3 使用数据用途
- **功能优化**:了解哪些功能最受欢迎,优先改进
- **用户体验改进**:优化界面设计和交互流程
- **统计分析**:了解用户规模和使用趋势
- **产品决策**:基于数据做出产品发展方向决策
---
## 3. 数据存储和处理
### 3.1 数据存储位置
我们使用以下第三方服务存储和处理数据:
#### Sentry崩溃报告
- **用途**:崩溃数据收集和分析
- **位置**:美国
- **官网**https://sentry.io
- **隐私政策**https://sentry.io/privacy/
#### PostHog使用分析
- **用途**:用户行为分析和统计
- **位置**:美国
- **官网**https://posthog.com
- **隐私政策**https://posthog.com/privacy
### 3.2 数据保留期限
- **崩溃数据**保留90天后自动删除
- **使用数据**保留12个月后自动删除
- **设备标识符**:永久保留(用于统计日活用户)
### 3.3 数据安全措施
我们采取以下安全措施保护您的数据:
- ✅ 数据传输使用HTTPS加密
- ✅ 数据存储使用加密技术
- ✅ 访问权限严格控制
- ✅ 定期安全审计
---
## 4. 数据共享
### 4.1 我们不会出售您的数据
我们**明确承诺**
- ❌ 不会出售您的个人数据
- ❌ 不会将您的数据用于广告目的
- ❌ 不会与第三方共享可识别个人的数据
### 4.2 必要的共享
我们仅在以下情况下共享数据:
- **服务提供商**与Sentry和PostHog共享数据以提供服务
- **法律要求**:在法律要求或政府机构合法要求时
---
## 5. 您的权利
### 5.1 选择权
您完全控制详细数据收集:
- **匿名崩溃数据**:可在设置中开启或关闭
- **匿名使用数据**:可在设置中开启或关闭
- **基础数据**:始终收集(用于统计用户数量)
**注意:** 即使关闭所有开关,我们仍会收集基础数据(应用启动事件和设备标识符)以统计用户数量。
### 5.2 数据访问权
您可以:
- 查看我们收集的数据类型
- 了解数据的使用目的
- 了解数据的存储位置
### 5.3 数据删除权
您可以:
- 随时关闭详细数据收集功能
- 删除本地存储的设备标识符
- 联系我们删除已收集的数据
---
## 6. 设备标识符
### 6.1 什么是设备标识符?
设备标识符是一个随机生成的唯一字符串,用于:
- 统计日活用户数量
- 区分不同设备
- 分析用户使用趋势
### 6.2 设备标识符的特点
- **匿名性**:不包含任何个人信息
- **随机性**通过SHA256算法生成
- **唯一性**:每个设备有唯一标识符
- **持久性**即使刷新设备ID仍能关联到同一用户
- **可重置**:您可以在设置中刷新设备标识符
### 6.3 设备标识符刷新
当您刷新设备标识符时:
- 生成新的设备ID
- **持久用户ID保持不变**,确保仍能关联到同一用户
- 统计数据不会丢失
### 6.4 设备标识符示例
```
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
```
---
## 7. 儿童隐私保护
本应用不面向13岁以下儿童。我们不会故意收集儿童的个人信息。如果您发现我们无意中收集了儿童的数据请联系我们我们将立即删除相关数据。
---
## 8. 国际数据传输
由于我们的服务提供商位于美国,您的数据可能会被传输到美国。我们确保:
- 数据传输符合相关法律法规
- 服务提供商遵守GDPR等隐私法规
- 采取适当的安全措施保护数据
---
## 9. 隐私政策更新
我们可能会不时更新本隐私政策。更新时,我们将:
- 在本页面更新"最后更新日期"
- 在应用内通知您重大变更
- 继续使用应用即表示您同意更新后的政策
---
## 10. 联系我们
如果您对本隐私政策有任何疑问或建议,请通过以下方式联系我们:
- **GitHub Issues**https://github.com/wwiinnddyy/LanMountainDesktop/issues
- **电子邮件**[您的邮箱地址]
---
## 11. 法律依据
### 11.1 GDPR合规
如果您位于欧洲经济区EEA我们的数据处理基于
- **同意**:您明确同意数据收集
- **合法利益**:改进应用性能和用户体验
### 11.2 CCPA合规
如果您是加州居民,您有权:
- 知道我们收集了哪些数据
- 要求删除您的数据
- 选择退出数据销售(我们不销售数据)
---
## 12. 第三方链接
本应用可能包含第三方网站链接。我们不对这些网站的隐私政策负责。请阅读这些网站的隐私政策。
---
## 13. 数据收集示例
### 13.1 崩溃报告示例
```json
{
"event_id": "abc123",
"timestamp": "2024-01-01T12:00:00Z",
"device_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"app_version": "1.0.0",
"os_name": "Windows",
"os_version": "10.0.19041",
"error_message": "NullReferenceException",
"stack_trace": "..."
}
```
### 13.2 使用数据示例
```json
{
"event": "app_online",
"timestamp": "2024-01-01T12:00:00Z",
"distinct_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"properties": {
"app_version": "1.0.0",
"os_name": "Windows",
"event_type": "app_start",
"analytics_enabled": true
}
}
```
### 13.3 基础数据示例(始终收集)
```json
{
"event": "$pageview",
"timestamp": "2024-01-01T12:00:00Z",
"distinct_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"properties": {
"$current_url": "app://main",
"$title": "LanMountainDesktop"
}
}
```
---
## 14. 您的同意
使用本应用即表示您:
- ✅ 已阅读并理解本隐私政策
- ✅ 同意我们按照本政策收集和使用数据
- ✅ 了解您可以随时撤回同意(详细数据收集)
- ✅ 了解基础数据将始终收集以统计用户数量
---
## 15. 免责声明
本隐私政策仅适用于 LanMountainDesktop 应用。我们不对以下情况负责:
- 第三方服务的隐私政策
- 您自行分享的数据
- 不可抗力导致的数据泄露
---
**感谢您信任阑山桌面LanMountainDesktop**
我们承诺保护您的隐私,并持续改进我们的隐私保护措施。

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
@@ -21,6 +21,9 @@
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
<AvaloniaResource Include="Localization\**" />
<EmbeddedResource Include="Assets\Documents\Privacy.md" />
<EmbeddedResource Include="Localization\*.json" />
<None Include="Localization\*.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="Extensions\Components\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
@@ -52,15 +55,14 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.0" />
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" />
<PackageReference Include="PortAudioSharp2" Version="1.0.6" />
<PackageReference Include="MaterialColorUtilities" Version="0.3.0" />
<PackageReference Include="PostHog" Version="2.4.0" />
<PackageReference Include="Sentry" Version="4.0.0" />
<PackageReference Include="System.Runtime.WindowsRuntime" Version="4.7.0" />
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" Condition="('$(RuntimeIdentifier)' == '' and $([MSBuild]::IsOSPlatform('Windows')))&#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

@@ -99,6 +99,11 @@
"settings.privacy.crash_upload_description": "Help us improve application stability.",
"settings.privacy.usage_upload_title": "Anonymous usage data uploads",
"settings.privacy.usage_upload_description": "Help us improve application features.",
"settings.privacy.device_id_title": "Device ID",
"settings.privacy.device_id_description": "Unique identifier for this device. Click refresh to regenerate.",
"settings.privacy.refresh_device_id": "Refresh",
"settings.privacy.policy_hint_prefix": "For more details, please ",
"settings.privacy.view_policy": "view our privacy policy",
"settings.weather.title": "Weather",
"settings.weather.description": "Configure weather location, Xiaomi weather preview, and startup positioning behavior.",
"settings.weather.location_source_header": "Location Source",
@@ -554,6 +559,7 @@
"component_category.info": "Info",
"component_category.calculator": "Calculator",
"component_category.study": "Study",
"component_category.file": "File",
"component.date": "Calendar",
"component.month_calendar": "Month Calendar",
"component.lunar_calendar": "Lunar Calendar",
@@ -887,5 +893,7 @@
"placement.tile": "Tile",
"single_instance.notice.title": "App already running",
"single_instance.notice.description": "The app is already running. There is no need to click multiple times to open it.",
"single_instance.notice.button": "OK"
"single_instance.notice.button": "OK",
"market.status.install_success_restart_format": "✓ Plugin '{0}' installed successfully! Please restart the application to activate it.",
"market.dialog.restart_message_format": "Plugin '{0}' has been installed successfully.\n\nTo use this plugin, you need to restart the application now.\n\nWould you like to restart?"
}

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": "选择顶部状态栏显示的组件。",
@@ -104,6 +98,11 @@
"settings.privacy.crash_upload_description": "帮助我们提高应用稳定性。",
"settings.privacy.usage_upload_title": "匿名上传使用数据",
"settings.privacy.usage_upload_description": "帮助我们改善应用功能。",
"settings.privacy.device_id_title": "设备标识符",
"settings.privacy.device_id_description": "此设备的唯一标识符。点击刷新以重新生成。",
"settings.privacy.refresh_device_id": "刷新",
"settings.privacy.policy_hint_prefix": "了解更多详情,请",
"settings.privacy.view_policy": "查看我们的隐私政策",
"settings.weather.title": "天气",
"settings.weather.description": "配置天气位置、小米天气预览和启动时的位置刷新行为。",
"settings.weather.location_source_header": "位置来源",
@@ -393,7 +392,6 @@
"settings.footer": "LanMountainDesktop 设置",
"filepicker.title": "选择壁纸",
"filepicker.image_files": "图片文件",
"filepicker.video_files": "视频文件",
"common.day": "日间",
"common.night": "夜间",
"common.back": "返回",
@@ -559,6 +557,7 @@
"component_category.info": "信息推荐",
"component_category.calculator": "计算器",
"component_category.study": "自习",
"component_category.file": "文件",
"component.date": "日历",
"component.month_calendar": "月历",
"component.lunar_calendar": "农历",
@@ -892,5 +891,7 @@
"placement.tile": "平铺",
"single_instance.notice.title": "应用已经运行",
"single_instance.notice.description": "应用已经运行,无需多次点击打开。",
"single_instance.notice.button": "确定"
"single_instance.notice.button": "确定",
"market.status.install_success_restart_format": "✓ 插件'{0}'安装成功!请重启应用以激活它。",
"market.dialog.restart_message_format": "插件'{0}'已成功安装。\n\n要使用此插件您需要立即重启应用。\n\n是否立即重启"
}

View File

@@ -62,8 +62,6 @@ public sealed class AppSettingsSnapshot
public string AppRenderMode { get; set; } = "Default";
public bool AutoCheckUpdates { get; set; } = true;
public bool IncludePrereleaseUpdates { get; set; }
public bool UploadAnonymousCrashData { get; set; }
@@ -72,6 +70,8 @@ public sealed class AppSettingsSnapshot
public string? DeviceId { get; set; }
public string? PersistentUserId { get; set; }
public string UpdateChannel { get; set; } = "stable";
public string UpdateMode { get; set; } = "download_then_confirm";

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();
@@ -477,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;
@@ -490,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,
@@ -886,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 },
_ => []
};
@@ -920,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) &&
@@ -960,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

@@ -15,6 +15,7 @@ public sealed class DeviceIdService
{
private static DeviceIdService? _instance;
private string? _deviceId;
private string? _persistentUserId; // 持久化的用户ID用于关联设备
private readonly ISettingsFacadeService _settingsFacade;
private bool _isInitialized;
@@ -43,6 +44,19 @@ public sealed class DeviceIdService
}
}
// 持久化的用户ID用于跨设备关联用户
public string PersistentUserId
{
get
{
if (_persistentUserId is null)
{
throw new InvalidOperationException("PersistentUserId not initialized");
}
return _persistentUserId;
}
}
private void EnsureDeviceId()
{
if (_isInitialized)
@@ -56,13 +70,22 @@ public sealed class DeviceIdService
{
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
// 初始化或生成持久化用户ID只生成一次永不改变
if (string.IsNullOrEmpty(snapshot.PersistentUserId))
{
snapshot.PersistentUserId = GeneratePersistentUserId();
AppLogger.Info("DeviceId", $"Generated new persistent user ID: {snapshot.PersistentUserId}");
}
_persistentUserId = snapshot.PersistentUserId;
// 初始化或生成设备ID可以刷新
if (string.IsNullOrEmpty(snapshot.DeviceId))
{
snapshot.DeviceId = GenerateDeviceId();
_settingsFacade.Settings.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys: [nameof(AppSettingsSnapshot.DeviceId)]);
changedKeys: [nameof(AppSettingsSnapshot.DeviceId), nameof(AppSettingsSnapshot.PersistentUserId)]);
_deviceId = snapshot.DeviceId;
AppLogger.Info("DeviceId", $"Generated new device ID: {_deviceId}");
}
@@ -75,6 +98,7 @@ public sealed class DeviceIdService
catch (Exception ex)
{
_deviceId = GenerateDeviceId();
_persistentUserId = GeneratePersistentUserId();
AppLogger.Warn("DeviceId", $"Failed to persist device ID, using generated ID: {_deviceId}", ex);
}
}
@@ -87,6 +111,15 @@ public sealed class DeviceIdService
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(deviceInfo));
return Convert.ToHexString(hash)[..32].ToLower();
}
private static string GeneratePersistentUserId()
{
// 生成一个永久性的用户ID基于机器名和用户名的哈希
var userInfo = $"{Environment.MachineName}|{Environment.UserName}|LanMountainDesktop";
using var sha = System.Security.Cryptography.SHA256.Create();
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(userInfo));
return Convert.ToHexString(hash)[..32].ToLower();
}
}
public sealed class UserBehaviorAnalyticsService : IDisposable
@@ -140,9 +173,18 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30));
// 发送PostHog标准的$pageview事件用于统计日活始终发送不受开关影响
CaptureEvent("$pageview", new Dictionary<string, object>
{
{ "$current_url", "app://main" },
{ "$title", "LanMountainDesktop" }
});
// 发送应用启动事件(始终发送,用于统计用户数量)
CaptureEvent("app_online", new Dictionary<string, object>
{
{ "event_type", "app_start" }
{ "event_type", "app_start" },
{ "analytics_enabled", _isEnabled }
});
AppLogger.Info("UserBehaviorAnalytics", $"Analytics initialized. DeviceId={_deviceIdService.DeviceId}, Enabled={_isEnabled}");
@@ -382,10 +424,22 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
try
{
// 基础事件($pageview, app_online, app_shutdown等始终发送用于统计用户数量
bool isBasicEvent = eventName.StartsWith("$") ||
eventName == "app_online" ||
eventName == "app_shutdown" ||
eventName == "$identify";
// 非基础事件只有在启用时才发送
if (!isBasicEvent && !_isEnabled)
{
return;
}
var eventData = new UserBehaviorEvent
{
Event = eventName,
DistinctId = _deviceIdService.DeviceId,
DistinctId = _deviceIdService.PersistentUserId, // 使用持久化用户ID
Timestamp = DateTimeOffset.UtcNow,
Properties = properties ?? new Dictionary<string, object>(),
IncludeDetailedData = _isEnabled
@@ -542,21 +596,29 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
{
var userProperties = new Dictionary<string, object>
{
{ "$device_id", distinctId },
{ "$app_version", GetAppVersion() },
{ "$os", GetOsName() },
{ "$os_version", GetOsVersion() }
{ "$os_version", GetOsVersion() },
{ "$device_type", GetDeviceModel() },
{ "$device_id", _deviceIdService.DeviceId } // 当前设备ID
};
// PostHog正确的$identify格式
// 使用PersistentUserId作为distinct_id确保设备ID刷新后仍能关联到同一用户
var requestBody = new Dictionary<string, object>
{
{ "api_key", PostHogApiKey },
{ "event", "$identify" },
{ "distinct_id", _deviceIdService.PersistentUserId }, // 使用持久化用户ID
{ "timestamp", DateTimeOffset.UtcNow.ToString("o") },
{ "properties", new Dictionary<string, object>
{
{ "distinct_id", distinctId },
{ "$set", userProperties }
{ "$set", userProperties },
{ "$set_once", new Dictionary<string, object>
{
{ "first_app_open", DateTimeOffset.UtcNow.ToString("o") }
}
}
}
}
};
@@ -796,6 +858,9 @@ public sealed class CrashReportService
});
ConfigureCrashReportingScope();
// 显式开始会话跟踪
SentrySdk.StartSession();
AppLogger.Info("CrashReport", $"Sentry crash reporting initialized. DeviceId={_deviceIdService.DeviceId}");
@@ -849,7 +914,10 @@ public sealed class CrashReportService
{
if (_isEnabled && _isInitialized)
{
AppLogger.Info("CrashReport", $"Shutdown event will be sent via Sentry. DeviceId={_deviceIdService.DeviceId}");
// 结束Sentry会话
SentrySdk.EndSession();
SentrySdk.Flush(TimeSpan.FromSeconds(3));
AppLogger.Info("CrashReport", $"Shutdown event sent via Sentry. DeviceId={_deviceIdService.DeviceId}");
return;
}

View File

@@ -1,6 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text.Json;
namespace LanMountainDesktop.Services;
@@ -16,6 +17,23 @@ public sealed class LocalizationService
private readonly Dictionary<string, Dictionary<string, string>> _cache =
new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// 清除指定语言代码的缓存,强制下次重新加载。
/// 在语言切换时调用此方法以确保加载最新的语言文件。
/// </summary>
public void ClearCache(string? languageCode = null)
{
if (string.IsNullOrWhiteSpace(languageCode))
{
_cache.Clear();
}
else
{
var normalizedCode = NormalizeLanguageCode(languageCode);
_cache.Remove(normalizedCode);
}
}
public string NormalizeLanguageCode(string? languageCode)
{
return string.Equals(languageCode, "en-US", StringComparison.OrdinalIgnoreCase)
@@ -42,14 +60,17 @@ public sealed class LocalizationService
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
try
{
var filePath = Path.Combine(AppContext.BaseDirectory, "Localization", $"{languageCode}.json");
if (File.Exists(filePath))
var json = TryLoadFromFileSystem(languageCode);
if (string.IsNullOrEmpty(json))
{
json = TryLoadFromEmbeddedResource(languageCode);
}
if (!string.IsNullOrEmpty(json))
{
var json = File.ReadAllText(filePath);
// Defensive: tolerate accidentally duplicated UTF-8 BOM characters at file start.
json = json.TrimStart('\uFEFF');
var data = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonOptions);
if (data is not null)
if (data is not null && data.Count > 0)
{
result = new Dictionary<string, string>(data, StringComparer.OrdinalIgnoreCase);
}
@@ -60,7 +81,48 @@ public sealed class LocalizationService
// Keep empty table for resilience.
}
_cache[languageCode] = result;
// 只有当语言表非空时才缓存,这样如果加载失败可以下次重试
if (result.Count > 0)
{
_cache[languageCode] = result;
}
return result;
}
private string? TryLoadFromFileSystem(string languageCode)
{
try
{
var filePath = Path.Combine(AppContext.BaseDirectory, "Localization", $"{languageCode}.json");
if (File.Exists(filePath))
{
return File.ReadAllText(filePath);
}
}
catch
{
// Continue to next method
}
return null;
}
private string? TryLoadFromEmbeddedResource(string languageCode)
{
try
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"LanMountainDesktop.Localization.{languageCode}.json";
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream != null)
{
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
}
catch
{
// Continue to next method
}
return null;
}
}

View File

@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using LanMountainDesktop.Services.Settings;
using Microsoft.Win32;
namespace LanMountainDesktop.Services;
@@ -35,66 +36,19 @@ public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService
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;
}
// 方法1: 从注册表读取Office最近文档最可靠
TryGetFromRegistry(documents);
try
{
var files = Directory.GetFiles(recentPath, "*.lnk");
foreach (var lnkPath in files)
{
var targetPath = GetShortcutTarget(lnkPath);
if (string.IsNullOrEmpty(targetPath))
{
continue;
}
// 方法2: 从Recent文件夹读取快捷方式备用
TryGetFromRecentFolders(documents);
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
{
}
}
// 方法3: 从Windows Jump List读取如果可用
TryGetFromJumpList(documents);
return documents
.GroupBy(d => d.FilePath, StringComparer.OrdinalIgnoreCase)
.Select(g => g.OrderByDescending(d => d.LastModifiedTime).First())
.OrderByDescending(d => d.LastModifiedTime)
.Take(maxCount)
.ToList();
@@ -116,6 +70,231 @@ public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService
}
}
#pragma warning disable CA1416 // 平台兼容性警告
private void TryGetFromRegistry(List<OfficeRecentDocument> documents)
{
try
{
// Word最近文档
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\Word\Reading Locations");
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Word\Reading Locations");
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\15.0\Word\Reading Locations");
// Excel最近文档
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\Excel\Reading Locations");
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Excel\Reading Locations");
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\15.0\Excel\Reading Locations");
// PowerPoint最近文档
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\PowerPoint\Reading Locations");
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\PowerPoint\Reading Locations");
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\15.0\PowerPoint\Reading Locations");
// 通用Office最近文档
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Common\Open Find\Microsoft Office Word");
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Common\Open Find\Microsoft Office Excel");
TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Common\Open Find\Microsoft Office PowerPoint");
}
catch
{
// 忽略注册表访问错误
}
}
private void TryGetFromOfficeRegistry(List<OfficeRecentDocument> documents, string registryPath)
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(registryPath);
if (key == null) return;
foreach (var subKeyName in key.GetSubKeyNames())
{
try
{
using var subKey = key.OpenSubKey(subKeyName);
if (subKey == null) continue;
var filePath = subKey.GetValue("Path") as string;
if (string.IsNullOrEmpty(filePath)) continue;
AddDocumentIfExists(documents, filePath);
}
catch
{
// 忽略单个子键访问错误
}
}
}
catch
{
// 忽略注册表访问错误
}
}
#pragma warning restore CA1416 // 平台兼容性警告
private void TryGetFromRecentFolders(List<OfficeRecentDocument> documents)
{
var recentPaths = GetRecentFolders();
foreach (var recentPath in recentPaths)
{
if (!Directory.Exists(recentPath))
{
continue;
}
try
{
var files = Directory.GetFiles(recentPath, "*.lnk");
foreach (var lnkPath in files)
{
var targetPath = GetShortcutTarget(lnkPath);
if (string.IsNullOrEmpty(targetPath))
{
continue;
}
AddDocumentIfExists(documents, targetPath);
}
}
catch
{
// 忽略文件夹访问错误
}
}
}
private void TryGetFromJumpList(List<OfficeRecentDocument> documents)
{
try
{
// Windows Jump List存储在以下位置
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var jumpListPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft", "Windows", "Recent", "AutomaticDestinations");
if (!Directory.Exists(jumpListPath)) return;
// Office应用的Jump List文件
var officeJumpListFiles = new[]
{
"a7bd7a3f3d5a4c74.automaticDestinations-ms", // Word
"9b524fe3be704a4d.automaticDestinations-ms", // Excel
"d0063c4c7de64e5e.automaticDestinations-ms" // PowerPoint
};
foreach (var jumpFile in officeJumpListFiles)
{
var fullPath = Path.Combine(jumpListPath, jumpFile);
if (File.Exists(fullPath))
{
TryParseJumpListFile(fullPath, documents);
}
}
}
catch
{
// Jump List解析失败忽略
}
}
private void TryParseJumpListFile(string jumpListPath, List<OfficeRecentDocument> documents)
{
try
{
// Jump List文件是二进制格式这里使用简化的方法
// 读取文件并尝试提取文件路径
var bytes = File.ReadAllBytes(jumpListPath);
var text = Encoding.Unicode.GetString(bytes);
// 查找可能的文件路径(简化实现)
var possiblePaths = ExtractPossiblePaths(text);
foreach (var path in possiblePaths)
{
AddDocumentIfExists(documents, path);
}
}
catch
{
// Jump List解析失败忽略
}
}
private IEnumerable<string> ExtractPossiblePaths(string text)
{
var paths = new List<string>();
// 查找常见的文件路径模式
var patterns = new[]
{
@"[A-Z]:\\[^\x00-\x1F""<>|]*\.(docx?|xlsx?|pptx?|rtf|csv)",
@"\\\\[^\\]+\\[^\x00-\x1F""<>|]*\.(docx?|xlsx?|pptx?|rtf|csv)"
};
foreach (var pattern in patterns)
{
try
{
var matches = System.Text.RegularExpressions.Regex.Matches(text, pattern,
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
foreach (System.Text.RegularExpressions.Match match in matches)
{
var path = match.Value.Trim('\0', ' ', '"');
if (!string.IsNullOrEmpty(path))
{
paths.Add(path);
}
}
}
catch
{
// 忽略正则表达式错误
}
}
return paths.Distinct(StringComparer.OrdinalIgnoreCase);
}
private void AddDocumentIfExists(List<OfficeRecentDocument> documents, string filePath)
{
try
{
var extension = Path.GetExtension(filePath).ToLowerInvariant();
if (!IsOfficeFile(extension))
{
return;
}
if (!File.Exists(filePath))
{
return;
}
var fileInfo = new FileInfo(filePath);
var doc = new OfficeRecentDocument
{
FileName = Path.GetFileNameWithoutExtension(filePath),
FilePath = filePath,
Extension = extension,
LastModifiedTime = fileInfo.LastWriteTime,
FileSizeBytes = fileInfo.Length,
IconGlyph = GetIconGlyph(extension)
};
if (!documents.Any(d => string.Equals(d.FilePath, filePath, StringComparison.OrdinalIgnoreCase)))
{
documents.Add(doc);
}
}
catch
{
// 忽略单个文件处理错误
}
}
private static List<string> GetRecentFolders()
{
var folders = new List<string>();
@@ -125,6 +304,12 @@ public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService
folders.Add(Path.Combine(appData, "Microsoft", "Excel", "Recent"));
folders.Add(Path.Combine(appData, "Microsoft", "PowerPoint", "Recent"));
// 添加Office 365路径
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
folders.Add(Path.Combine(localAppData, "Microsoft", "Office", "Word", "Recent"));
folders.Add(Path.Combine(localAppData, "Microsoft", "Office", "Excel", "Recent"));
folders.Add(Path.Combine(localAppData, "Microsoft", "Office", "PowerPoint", "Recent"));
return folders;
}

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,
@@ -48,7 +47,6 @@ public sealed record PrivacySettingsState(
bool UploadAnonymousCrashData,
bool UploadAnonymousUsageData);
public sealed record UpdateSettingsState(
bool AutoCheckUpdates,
bool IncludePrereleaseUpdates,
string UpdateChannel,
string UpdateMode,

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;
}
@@ -638,7 +628,6 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
snapshot.UpdateChannel,
snapshot.IncludePrereleaseUpdates);
return new UpdateSettingsState(
snapshot.AutoCheckUpdates,
string.Equals(normalizedChannel, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase),
normalizedChannel,
UpdateSettingsValues.NormalizeMode(snapshot.UpdateMode),
@@ -656,7 +645,6 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
var normalizedChannel = UpdateSettingsValues.NormalizeChannel(
state.UpdateChannel,
state.IncludePrereleaseUpdates);
snapshot.AutoCheckUpdates = state.AutoCheckUpdates;
snapshot.IncludePrereleaseUpdates = string.Equals(
normalizedChannel,
UpdateSettingsValues.ChannelPreview,
@@ -682,7 +670,7 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.AutoCheckUpdates),
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
nameof(AppSettingsSnapshot.UpdateChannel),
nameof(AppSettingsSnapshot.UpdateMode),

View File

@@ -294,6 +294,8 @@ internal sealed class SettingsWindowService : ISettingsWindowService
if (languageChanged)
{
var regionState = _settingsFacade.Region.Get();
// 清除本地化缓存,强制重新加载语言文件
_localizationService.ClearCache();
_viewModel.RefreshLanguage(regionState.LanguageCode);
_pageRegistry.Rebuild();
_window.ReloadPages(_viewModel.CurrentPageId);

View File

@@ -131,13 +131,10 @@ public sealed class UpdateWorkflowService
CancellationToken cancellationToken = default)
{
var state = _settingsFacade.Update.Get();
if (!state.AutoCheckUpdates)
{
return;
}
try
{
// Always check for updates on startup (removed AutoCheckUpdates check)
var result = await CheckForUpdatesAsync(currentVersion, cancellationToken);
if (!result.Success || !result.IsUpdateAvailable || result.PreferredAsset is null)
{
@@ -145,12 +142,14 @@ public sealed class UpdateWorkflowService
}
var normalizedMode = UpdateSettingsValues.NormalizeMode(state.UpdateMode);
if (string.Equals(normalizedMode, UpdateSettingsValues.ModeManual, StringComparison.OrdinalIgnoreCase))
// For "Silent Download" and "Silent Install" modes, automatically download the update
if (string.Equals(normalizedMode, UpdateSettingsValues.ModeDownloadThenConfirm, StringComparison.OrdinalIgnoreCase) ||
string.Equals(normalizedMode, UpdateSettingsValues.ModeSilentOnExit, StringComparison.OrdinalIgnoreCase))
{
return;
await DownloadReleaseAsync(result, cancellationToken: cancellationToken);
}
await DownloadReleaseAsync(result, cancellationToken: cancellationToken);
// For "Manual" mode, just check but don't download
}
catch (OperationCanceledException)
{

View File

@@ -564,11 +564,19 @@ public sealed partial class PluginMarketSettingsPageViewModel : ViewModelBase
{
RefreshInstalledSnapshot();
RefreshItemStates();
// 设置更明显的状态消息
var pluginName = result.PluginName ?? item.Name;
StatusMessage = string.Format(
CultureInfo.CurrentCulture,
L("market.status.install_success_format", "Plugin '{0}' has been staged. Restart the app to apply it."),
result.PluginName ?? item.Name);
RestartRequested?.Invoke(RestartRequiredMessage);
L("market.status.install_success_restart_format", "Plugin '{0}' installed successfully! Please restart the application to activate it."),
pluginName);
// 触发重启提醒
RestartRequested?.Invoke(string.Format(
CultureInfo.CurrentCulture,
L("market.dialog.restart_message_format", "Plugin '{0}' has been installed successfully.\n\nTo use this plugin, you need to restart the application now.\n\nWould you like to restart?"),
pluginName));
return;
}

View File

@@ -0,0 +1,92 @@
using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.ViewModels;
public sealed partial class PrivacyPolicyViewModel : ViewModelBase
{
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
[ObservableProperty]
private string _title = string.Empty;
[ObservableProperty]
private string _description = string.Empty;
[ObservableProperty]
private string _loadingText = string.Empty;
[ObservableProperty]
private string _errorText = string.Empty;
[ObservableProperty]
private string _markdownContent = string.Empty;
[ObservableProperty]
private bool _isLoading = true;
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _hasContent;
public PrivacyPolicyViewModel()
{
_languageCode = "zh-CN";
RefreshLocalizedText();
LoadPrivacyPolicy();
}
private void RefreshLocalizedText()
{
Title = L("settings.privacy.policy_title", "Privacy Policy");
Description = L("settings.privacy.policy_description", "Learn how we collect, use, and protect your data.");
LoadingText = L("settings.privacy.policy_loading", "Loading privacy policy...");
}
private async void LoadPrivacyPolicy()
{
try
{
IsLoading = true;
HasError = false;
HasContent = false;
// 从嵌入资源加载隐私政策Markdown文件
var assembly = Assembly.GetExecutingAssembly();
var resourceName = "LanMountainDesktop.Assets.Documents.Privacy.md";
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream is null)
{
throw new FileNotFoundException($"Privacy policy resource not found: {resourceName}");
}
using var reader = new StreamReader(stream);
var markdown = await reader.ReadToEndAsync();
MarkdownContent = markdown;
IsLoading = false;
HasContent = true;
AppLogger.Info("PrivacyPolicy", "Privacy policy loaded successfully.");
}
catch (Exception ex)
{
AppLogger.Warn("PrivacyPolicy", "Failed to load privacy policy.", ex);
IsLoading = false;
HasError = true;
ErrorText = L("settings.privacy.policy_error", "Failed to load privacy policy. Please try again later.");
}
}
private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback);
}

View File

@@ -15,6 +15,8 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
private readonly string _languageCode;
private bool _isInitializing;
public event Action? ViewPrivacyPolicyRequested;
public PrivacySettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
_settingsFacade = settingsFacade;
@@ -59,6 +61,12 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
[ObservableProperty]
private string _refreshDeviceIdText = string.Empty;
[ObservableProperty]
private string _viewPrivacyPolicyText = string.Empty;
[ObservableProperty]
private string _privacyPolicyHintPrefix = string.Empty;
public void Load()
{
var state = _settingsFacade.Privacy.Get();
@@ -130,6 +138,25 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
DeviceIdHeader = L("settings.privacy.device_id_title", "Device ID");
DeviceIdDescription = L("settings.privacy.device_id_description", "Unique identifier for this device. Click refresh to regenerate.");
RefreshDeviceIdText = L("settings.privacy.refresh_device_id", "Refresh");
PrivacyPolicyHintPrefix = L("settings.privacy.policy_hint_prefix", "For more details, please ");
ViewPrivacyPolicyText = L("settings.privacy.view_policy", "view our privacy policy");
}
[RelayCommand]
private void ViewPrivacyPolicy()
{
try
{
// 触发隐私政策查看事件
AppLogger.Info("PrivacySettings", "User requested to view privacy policy.");
// 发送事件通知显示隐私政策
ViewPrivacyPolicyRequested?.Invoke();
}
catch (Exception ex)
{
AppLogger.Warn("PrivacySettings", "Failed to view privacy policy.", ex);
}
}
private string L(string key, string fallback)

View File

@@ -268,12 +268,17 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase
partial void OnSelectedLanguageChanged(SelectionOption value)
{
RefreshPreview();
if (_isInitializing || value is null)
{
return;
}
// 更新语言代码并刷新UI文本
_languageCode = _localizationService.NormalizeLanguageCode(value.Value);
RefreshLocalizedText();
RefreshPreview();
// 保存设置
_settingsFacade.Region.Save(new RegionSettingsState(
value.Value,
NormalizeTimeZoneId(SelectedTimeZone?.Id)));
@@ -1329,9 +1334,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
LoadStateFromSettings();
}
[ObservableProperty]
private bool _autoCheckUpdates;
[ObservableProperty]
private string _selectedUpdateChannelValue = UpdateSettingsValues.ChannelStable;
@@ -1380,9 +1382,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private string _preferencesDescription = string.Empty;
[ObservableProperty]
private string _autoCheckUpdatesLabel = string.Empty;
[ObservableProperty]
private string _updateChannelLabel = string.Empty;
@@ -1520,16 +1519,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
private bool IsBusy => IsCheckingForUpdates || IsDownloading;
partial void OnAutoCheckUpdatesChanged(bool value)
{
if (_isInitializing)
{
return;
}
SaveUpdateSettings();
}
partial void OnSelectedUpdateChannelOptionChanged(SelectionOption? value)
{
if (value is not null &&
@@ -1729,7 +1718,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
var current = _settingsFacade.Update.Get();
_settingsFacade.Update.Save(current with
{
AutoCheckUpdates = AutoCheckUpdates,
IncludePrereleaseUpdates = string.Equals(
SelectedUpdateChannelValue,
UpdateSettingsValues.ChannelPreview,
@@ -1841,7 +1829,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
StatusCardDescription = L("settings.update.status_card_description", "Check for updates and review the latest release information.");
PreferencesHeader = L("settings.update.preferences_header", "Update Preferences");
PreferencesDescription = L("settings.update.preferences_description", "Choose your release channel, download source, behavior, and download speed.");
AutoCheckUpdatesLabel = L("settings.update.auto_check_toggle", "Automatically check for updates on startup");
UpdateChannelLabel = L("settings.update.channel_label", "Update Channel");
UpdateSourceLabel = L("settings.update.source_label", "Download Source");
UpdateModeLabel = L("settings.update.mode_label", "Update Mode");
@@ -1870,7 +1857,6 @@ public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
{
var update = _settingsFacade.Update.Get();
_isInitializing = true;
AutoCheckUpdates = update.AutoCheckUpdates;
SelectedUpdateChannelValue = UpdateSettingsValues.NormalizeChannel(update.UpdateChannel, update.IncludePrereleaseUpdates);
SelectedUpdateSourceValue = UpdateSettingsValues.NormalizeDownloadSource(update.UpdateDownloadSource);
SelectedUpdateModeValue = UpdateSettingsValues.NormalizeMode(update.UpdateMode);

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

@@ -43,7 +43,7 @@
<Grid Grid.Row="1">
<ScrollViewer x:Name="ContentScrollViewer"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Disabled">
VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="CourseListPanel" />
</ScrollViewer>

View File

@@ -198,12 +198,32 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
return;
}
if (courseIndex < CourseListPanel.Children.Count)
// 确保在UI线程执行
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
if (courseIndex >= CourseListPanel.Children.Count)
{
return;
}
var targetChild = CourseListPanel.Children[courseIndex];
if (targetChild == null || !targetChild.IsArrangeValid)
{
return;
}
var bounds = targetChild.Bounds;
ContentScrollViewer.Offset = new Vector(0, bounds.Position.Y);
}
var scrollViewerHeight = ContentScrollViewer.Bounds.Height;
var contentHeight = CourseListPanel.Bounds.Height;
// 计算滚动位置,使当前课程居中显示
var targetOffset = bounds.Position.Y - (scrollViewerHeight / 2) + (bounds.Height / 2);
// 确保不超出边界
targetOffset = Math.Max(0, Math.Min(targetOffset, contentHeight - scrollViewerHeight));
ContentScrollViewer.Offset = new Vector(0, targetOffset);
}, Avalonia.Threading.DispatcherPriority.Loaded);
}
public void RefreshFromSettings()
@@ -298,6 +318,15 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
var currentIndex = FindCurrentCourseIndex();
_lastCurrentCourseIndex = currentIndex;
HideStatus();
// 初始化时自动跳转到当前课程
if (currentIndex >= 0)
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
ScrollToCurrentCourse(currentIndex);
}, Avalonia.Threading.DispatcherPriority.Loaded);
}
}
RenderScheduleItems();
@@ -484,10 +513,9 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
: CreateBrush("#FF4D5A");
var normalBulletBrush = CreateBrush(_isNightVisual ? "#B8BEC9" : "#9AA3B2");
var visibleItems = _courseItems.Take(maxVisibleItems).ToList();
for (var i = 0; i < visibleItems.Count; i++)
for (var i = 0; i < _courseItems.Count; i++)
{
var item = visibleItems[i];
var item = _courseItems[i];
var bulletBrush = item.IsCurrent ? currentBrush : normalBulletBrush;
var bullet = new Border

View File

@@ -11,7 +11,7 @@
<Border x:Name="RootBorder"
CornerRadius="34"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
Background="#2D5A8E"
ClipToBounds="True"
BorderThickness="0"
Padding="0">
@@ -23,15 +23,15 @@
VerticalAlignment="Top"
Margin="0,-40,-40,0"
CornerRadius="70"
Background="{DynamicResource SystemAccentColorLight2Brush}"
Opacity="0.2"
Background="#4A90D9"
Opacity="0.3"
IsHitTestVisible="False" />
<Grid RowDefinitions="Auto,*" RowSpacing="8" Margin="16,14,16,14">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<TextBlock x:Name="HeaderTextBlock"
Text="最近文档"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="#D8FFFFFF"
FontSize="18"
FontWeight="SemiBold"
VerticalAlignment="Center" />
@@ -48,7 +48,7 @@
PointerPressed="OnRefreshPointerPressed">
<fi:SymbolIcon Symbol="ArrowSync"
FontSize="14"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
Foreground="#B8FFFFFF" />
</Button>
</Grid>
@@ -68,14 +68,14 @@
Width="130"
Height="90"
CornerRadius="10"
Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
Background="#3AFFFFFF"
Padding="10"
Cursor="Hand"
PointerPressed="OnDocumentCardPointerPressed">
<Grid RowDefinitions="Auto,*,Auto">
<TextBlock Grid.Row="0"
Text="{Binding FileName}"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="#D8FFFFFF"
FontSize="12"
FontWeight="Medium"
TextTrimming="CharacterEllipsis"
@@ -84,7 +84,7 @@
VerticalAlignment="Top" />
<TextBlock Grid.Row="2"
Text="{Binding TimeAgo}"
Foreground="{DynamicResource AdaptiveTextTertiaryBrush}"
Foreground="#9AFFFFFF"
FontSize="10"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
@@ -99,7 +99,7 @@
<TextBlock x:Name="StatusTextBlock"
IsVisible="False"
Text="暂无最近文档"
Foreground="{DynamicResource AdaptiveTextTertiaryBrush}"
Foreground="#9AFFFFFF"
FontSize="14"
HorizontalAlignment="Center"
VerticalAlignment="Center" />

View File

@@ -2702,6 +2702,11 @@ public partial class MainWindow
return Symbol.Apps;
}
if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase))
{
return Symbol.Folder;
}
return Symbol.Apps;
}
@@ -2747,6 +2752,11 @@ public partial class MainWindow
return L("component_category.study", "Study");
}
if (string.Equals(categoryId, "File", StringComparison.OrdinalIgnoreCase))
{
return L("component_category.file", "File");
}
return categoryId;
}

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;
@@ -564,6 +473,7 @@ public partial class MainWindow
var latestWeatherState = _weatherSettingsService.Get();
var latestUpdateState = _updateSettingsService.Get();
var latestThemeState = _themeSettingsService.Get();
var latestPrivacyState = _settingsFacade.Privacy.Get();
return new AppSettingsSnapshot
{
GridShortSideCells = _targetShortSideCells,
@@ -595,7 +505,8 @@ public partial class MainWindow
WeatherNoTlsRequests = latestWeatherState.NoTlsRequests,
AutoStartWithWindows = _autoStartWithWindows,
AppRenderMode = _selectedAppRenderMode,
AutoCheckUpdates = latestUpdateState.AutoCheckUpdates,
UploadAnonymousCrashData = latestPrivacyState.UploadAnonymousCrashData,
UploadAnonymousUsageData = latestPrivacyState.UploadAnonymousUsageData,
IncludePrereleaseUpdates = latestUpdateState.IncludePrereleaseUpdates,
UpdateChannel = latestUpdateState.UpdateChannel,
UpdateMode = latestUpdateState.UpdateMode,
@@ -644,112 +555,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

@@ -16,8 +16,11 @@
<ui:SettingsExpander.Footer>
<Grid ColumnDefinitions="*,Auto"
ColumnSpacing="12">
<TextBox Text="{Binding SearchText}"
Watermark="{Binding SearchPlaceholder}" />
<TextBox x:Name="SearchTextBox"
Text="{Binding SearchText}"
Watermark="{Binding SearchPlaceholder}"
Focusable="True"
IsTabStop="True" />
<Button Grid.Column="1"
Command="{Binding RefreshCommand}"
Content="{Binding RefreshButtonText}" />

View File

@@ -0,0 +1,55 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia"
xmlns:helpers="using:LanMountainDesktop.Helpers"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.PrivacyPolicyDrawer"
x:DataType="vm:PrivacyPolicyViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container"
Margin="0,0,0,8">
<Border Classes="settings-section-card">
<StackPanel Spacing="12">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="12">
<Border Classes="settings-section-card-icon-host"
Width="48"
Height="48">
<Viewbox Stretch="Uniform">
<fi:SymbolIcon Symbol="Document" />
</Viewbox>
</Border>
<StackPanel Grid.Column="1"
Spacing="4"
VerticalAlignment="Center">
<TextBlock Classes="settings-card-header"
Margin="0"
Text="{Binding Title}" />
<TextBlock Classes="settings-item-description"
Text="{Binding Description}" />
</StackPanel>
</Grid>
</StackPanel>
</Border>
<Border Classes="settings-section-card">
<StackPanel Spacing="12">
<TextBlock Classes="settings-item-description"
IsVisible="{Binding IsLoading}"
Text="{Binding LoadingText}" />
<TextBlock Classes="settings-item-description"
IsVisible="{Binding HasError}"
Text="{Binding ErrorText}"
TextWrapping="Wrap" />
<mdxaml:MarkdownScrollViewer IsVisible="{Binding HasContent}"
Markdown="{Binding MarkdownContent}"
Engine="{x:Static helpers:PluginMarketMarkdownHelper.Engine}" />
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,17 @@
using Avalonia.Controls;
using LanMountainDesktop.ViewModels;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class PrivacyPolicyDrawer : UserControl
{
public PrivacyPolicyDrawer()
{
InitializeComponent();
}
public PrivacyPolicyDrawer(PrivacyPolicyViewModel viewModel) : this()
{
DataContext = viewModel;
}
}

View File

@@ -45,10 +45,13 @@
FontSize="12"
Opacity="0.7"
Margin="0,4,0,8" />
<TextBox Text="{Binding DeviceId}"
<TextBox x:Name="DeviceIdTextBox"
Text="{Binding DeviceId}"
IsReadOnly="True"
FontFamily="Consolas"
FontSize="12" />
FontSize="12"
Focusable="False"
IsTabStop="False" />
</StackPanel>
<Button Grid.Column="1"
Content="{Binding RefreshDeviceIdText}"
@@ -58,6 +61,28 @@
Classes="accent-button" />
</Grid>
</Border>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
Spacing="4">
<TextBlock Text="{Binding PrivacyPolicyHintPrefix}"
FontSize="13"
VerticalAlignment="Center" />
<Button Content="{Binding ViewPrivacyPolicyText}"
Command="{Binding ViewPrivacyPolicyCommand}"
Background="Transparent"
BorderThickness="0"
Padding="0"
FontSize="13"
Foreground="{DynamicResource SystemAccentColor}"
Cursor="Hand">
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource SystemAccentColorDark1}" />
</Style>
</Button.Styles>
</Button>
</StackPanel>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -22,9 +22,17 @@ public partial class PrivacySettingsPage : SettingsPageBase
public PrivacySettingsPage(PrivacySettingsPageViewModel viewModel)
{
ViewModel = viewModel;
ViewModel.ViewPrivacyPolicyRequested += OnViewPrivacyPolicyRequested;
DataContext = ViewModel;
InitializeComponent();
}
public PrivacySettingsPageViewModel ViewModel { get; }
private void OnViewPrivacyPolicyRequested()
{
var privacyPolicyViewModel = new PrivacyPolicyViewModel();
var drawer = new PrivacyPolicyDrawer(privacyPolicyViewModel);
OpenDrawer(drawer, privacyPolicyViewModel.Title);
}
}

View File

@@ -21,12 +21,17 @@
<Setter Property="FontSize" Value="12" />
<Setter Property="Opacity" Value="0.68" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="MaxWidth" Value="200" />
</Style>
<Style Selector="TextBlock.update-kv-value">
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="MaxWidth" Value="200" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
</Style>
</UserControl.Styles>
@@ -64,9 +69,9 @@
Content="{Binding CheckForUpdatesButtonText}" />
</Grid>
<Grid ColumnDefinitions="*,*"
ColumnSpacing="14"
RowSpacing="12">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="20"
RowSpacing="16">
<StackPanel Grid.Column="0"
Spacing="4">
<TextBlock Classes="update-kv-label"
@@ -105,18 +110,23 @@
</StackPanel>
</Grid>
<StackPanel Spacing="8">
<StackPanel Spacing="12">
<TextBlock Classes="settings-item-description"
Text="{Binding UpdateStatus}" />
Text="{Binding UpdateStatus}"
TextWrapping="Wrap"
MaxWidth="500" />
<ProgressBar Minimum="0"
Maximum="100"
Value="{Binding DownloadProgressValue}"
IsVisible="{Binding IsDownloadProgressVisible}" />
IsVisible="{Binding IsDownloadProgressVisible}"
Margin="0,4,0,4" />
<TextBlock Classes="settings-item-description"
IsVisible="{Binding IsDownloadProgressVisible}"
Text="{Binding DownloadProgressText}" />
Text="{Binding DownloadProgressText}"
TextWrapping="Wrap"
Margin="0,4,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal"
@@ -210,15 +220,7 @@
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Classes="settings-expander-card"
Header="{Binding AutoCheckUpdatesLabel}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ClockAlarm" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding AutoCheckUpdates}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>

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>

View File

@@ -96,8 +96,11 @@
<StackPanel Spacing="14">
<Grid ColumnDefinitions="*,Auto"
ColumnSpacing="12">
<TextBox Text="{Binding SearchKeyword}"
Watermark="{Binding SearchPlaceholder}" />
<TextBox x:Name="SearchKeywordTextBox"
Text="{Binding SearchKeyword}"
Watermark="{Binding SearchPlaceholder}"
Focusable="True"
IsTabStop="True" />
<Button Grid.Column="1"
Command="{Binding SearchCommand}"
Content="{Binding SearchButtonText}" />
@@ -178,12 +181,18 @@
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<TextBox Text="{Binding LocationKey}"
Watermark="{Binding LocationKeyPlaceholder}" />
<TextBox x:Name="LocationKeyTextBox"
Text="{Binding LocationKey}"
Watermark="{Binding LocationKeyPlaceholder}"
Focusable="True"
IsTabStop="True" />
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<TextBox Text="{Binding LocationName}"
Watermark="{Binding LocationNamePlaceholder}" />
<TextBox x:Name="LocationNameTextBox"
Text="{Binding LocationName}"
Watermark="{Binding LocationNamePlaceholder}"
Focusable="True"
IsTabStop="True" />
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
@@ -230,11 +239,14 @@
<fi:SymbolIconSource Symbol="Warning" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<TextBox Width="360"
<TextBox x:Name="ExcludedAlertsTextBox"
Width="360"
MinHeight="120"
AcceptsReturn="True"
TextWrapping="Wrap"
Text="{Binding ExcludedAlerts}" />
Text="{Binding ExcludedAlerts}"
Focusable="True"
IsTabStop="True" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>