mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Compare commits
4 Commits
Avalonia12
...
v0.8.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc4d0c4cd8 | ||
|
|
eb066b53f1 | ||
|
|
5ea242af9a | ||
|
|
abfa64b3d7 |
106
.trae/documents/avalonia12-migration-plan.md
Normal file
106
.trae/documents/avalonia12-migration-plan.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Avalonia 12 迁移计划
|
||||
|
||||
## 当前状态
|
||||
|
||||
项目已完成以下迁移准备:
|
||||
|
||||
* `Directory.Packages.props` 中 Avalonia 包已升级到 `12.0.1`
|
||||
|
||||
* `FluentAvaloniaUI` 已升级到 `3.0.0-preview1`
|
||||
|
||||
* `Avalonia.Diagnostics` 已替换为 `AvaloniaUI.DiagnosticsSupport`
|
||||
|
||||
* `Avalonia.Controls.WebView` 已升级到 `12.0.0`
|
||||
|
||||
* `ClassIsland.Markdown.Avalonia` 已升级到 `12.0.0`
|
||||
|
||||
## 构建错误清单(26 errors)
|
||||
|
||||
### 1. 窗口装饰 API 移除(8 errors)
|
||||
|
||||
**文件**:`LanMountainDesktop/Views/SettingsWindow.axaml.cs`(4 errors)
|
||||
|
||||
* `ExtendClientAreaChromeHints` 不存在(line 166, 179)
|
||||
|
||||
* `SystemDecorations` 已过时,需改用 `WindowDecorations`(line 168, 177)
|
||||
|
||||
**文件**:`LanMountainDesktop/Views/ComponentEditorWindow.axaml.cs`(4 errors)
|
||||
|
||||
* `ExtendClientAreaChromeHints` 不存在(line 63, 72)
|
||||
|
||||
* `SystemDecorations` 已过时,需改用 `WindowDecorations`(line 65, 70)
|
||||
|
||||
**AXAML 文件**:13 个文件使用 `SystemDecorations` 属性(编译警告)
|
||||
|
||||
### 2. 变量/字段未找到(8 errors)
|
||||
|
||||
**文件**:`LanMountainDesktop/Views/MainWindow.ComponentSystem.cs`
|
||||
|
||||
* `centerLeft` 不存在(line 759, 766, 778)
|
||||
|
||||
* `positions` 不存在(line 1266)
|
||||
|
||||
**文件**:`LanMountainDesktop/Views/MainWindow.DesktopPaging.cs`
|
||||
|
||||
* `child` 不存在(line 312)
|
||||
|
||||
* `_isThreeFingerOrRightDragSwipeActive` 不存在(line 517, 828, 847, 850)
|
||||
|
||||
### 3. API 变更(3 errors)
|
||||
|
||||
**文件**:`LanMountainDesktop/App.axaml.cs`
|
||||
|
||||
* `BindingPlugins` 不可访问(line 532, 537)
|
||||
|
||||
**文件**:`LanMountainDesktop/Views/Components/DesktopComponentFailureView.cs`
|
||||
|
||||
* `IClipboard.SetTextAsync` 不存在(line 187)
|
||||
|
||||
**文件**:`LanMountainDesktop/Services/MonetColorService.cs`
|
||||
|
||||
* `Bitmap.CopyPixels` 参数不匹配(line 91)
|
||||
|
||||
### 4. 第三方库变更(1 error)
|
||||
|
||||
**文件**:`LanMountainDesktop/Views/SettingsWindow.axaml.cs`
|
||||
|
||||
* `FluentIcons.Avalonia.SymbolIconSource` 不存在(line 215)
|
||||
|
||||
### 5. 过时属性警告(需同步修复)
|
||||
|
||||
* `TextBox.Watermark` → `PlaceholderText`(7 处 .cs + 7 处 .axaml)
|
||||
|
||||
## 迁移步骤
|
||||
|
||||
### Phase 1: 修复窗口装饰 API(高优先级)
|
||||
|
||||
1. 重写 `SettingsWindow.ApplyChromeMode()` 使用 Avalonia 12 新 API
|
||||
2. 重写 `ComponentEditorWindow.ApplyChromeMode()` 使用 Avalonia 12 新 API
|
||||
3. 批量替换所有 `.axaml` 中的 `SystemDecorations` → `WindowDecorations`
|
||||
|
||||
### Phase 2: 修复 MainWindow 编译错误(高优先级)
|
||||
|
||||
1. 检查 `MainWindow.ComponentSystem.cs` 中 `centerLeft` 和 `positions` 的作用域问题
|
||||
2. 检查 `MainWindow.DesktopPaging.cs` 中 `child` 和 `_isThreeFingerOrRightDragSwipeActive` 的作用域问题
|
||||
3. 确认这些变量是否被意外删除或重命名
|
||||
|
||||
### Phase 3: 修复 Avalonia 12 API 变更(中优先级)
|
||||
|
||||
1. `App.axaml.cs`: 替换 `BindingPlugins.DataValidators` 的访问方式
|
||||
2. `DesktopComponentFailureView.cs`: 使用新的剪贴板 API
|
||||
3. `MonetColorService.cs`: 更新 `Bitmap.CopyPixels` 调用签名
|
||||
|
||||
### Phase 4: 修复第三方库变更(中优先级)
|
||||
|
||||
1. `SettingsWindow.axaml.cs`: 替换 `FluentIcons.Avalonia.SymbolIconSource` 为 v3 等效 API
|
||||
|
||||
### Phase 5: 清理过时属性(低优先级)
|
||||
|
||||
1. 批量替换 `Watermark` → `PlaceholderText`(所有 .cs 和 .axaml)
|
||||
|
||||
## 验证步骤
|
||||
|
||||
* 每阶段修复后运行 `dotnet build LanMountainDesktop.slnx -c Debug`
|
||||
|
||||
* 最终运行 `dotnet test LanMountainDesktop.slnx -c Debug`
|
||||
|
||||
21
.trae/specs/avalonia-12-migration/checklist.md
Normal file
21
.trae/specs/avalonia-12-migration/checklist.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Checklist
|
||||
|
||||
- [x] `Directory.Packages.props` contains the Avalonia 12 dependency baseline.
|
||||
- [x] Main host references `Avalonia.Controls.WebView`.
|
||||
- [x] Source no longer references `WebView.Avalonia`, `AvaloniaWebView`, or `.UseDesktopWebView()`.
|
||||
- [x] `BrowserWidget` uses `NativeWebView.Source`, `Navigate`, `Refresh()`, `NavigationStarted`, and `EnvironmentRequested`.
|
||||
- [x] WebView blanking navigates to `about:blank`.
|
||||
- [x] Plugin SDK package version is `5.0.0`.
|
||||
- [x] `PluginSdkInfo.ApiVersion` is `5.0.0`.
|
||||
- [x] Plugin template package version default is `5.0.0`.
|
||||
- [x] Plugin template manifest `apiVersion` is `5.0.0`.
|
||||
- [x] Launcher data location config resolution cannot recurse through `ResolveDataRoot()`.
|
||||
- [x] `OobeStateServiceTests` pass.
|
||||
- [x] `dotnet build LanMountainDesktop.slnx -c Debug` has 0 errors.
|
||||
- [x] `dotnet test LanMountainDesktop.slnx -c Debug` completes without a test host stack overflow.
|
||||
- [ ] Windows host smoke test completed.
|
||||
- [ ] Windows Launcher smoke test completed.
|
||||
- [ ] Settings window FluentAvalonia 3 smoke test completed.
|
||||
- [ ] Component editor Material smoke test completed.
|
||||
- [ ] BrowserWidget navigation/refresh/page activation smoke test completed.
|
||||
- [ ] WebView2 missing-runtime diagnostic smoke test completed.
|
||||
49
.trae/specs/avalonia-12-migration/spec.md
Normal file
49
.trae/specs/avalonia-12-migration/spec.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Avalonia 12 Full Stack Migration
|
||||
|
||||
## Summary
|
||||
|
||||
LanMountainDesktop has moved its desktop stack to the Avalonia 12 baseline. The migration covers the main host, Launcher, Plugin SDK, plugin runtime loading policy, official WebView usage, ClassIsland Markdown, FluentAvalonia, FluentIcons, and Material-related dependencies.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Centralized Avalonia 12 dependency baseline
|
||||
|
||||
The solution SHALL use central package management for direct Avalonia-facing projects and keep the core UI dependency baseline on Avalonia `12.0.1`.
|
||||
|
||||
Required package baseline:
|
||||
|
||||
- `Avalonia*` `12.0.1`
|
||||
- `Avalonia.Controls.WebView` `12.0.0`
|
||||
- `ClassIsland.Markdown.Avalonia` `12.0.0`
|
||||
- `FluentAvaloniaUI` `3.0.0-preview1`
|
||||
- `FluentIcons.Avalonia` `2.1.325`
|
||||
- `Material.Avalonia` `3.16.0`
|
||||
- `Material.Icons.Avalonia` `3.0.2`
|
||||
|
||||
### Requirement: Official WebView
|
||||
|
||||
The host SHALL use `Avalonia.Controls.NativeWebView` for the browser widget and SHALL NOT reference `WebView.Avalonia`, `AvaloniaWebView`, or `.UseDesktopWebView()`.
|
||||
|
||||
Windows WebView2 user data configuration SHALL be supplied through `EnvironmentRequested` using `WindowsWebView2EnvironmentRequestedEventArgs.UserDataFolder`.
|
||||
|
||||
### Requirement: Plugin SDK v5
|
||||
|
||||
The Plugin SDK API baseline SHALL be `5.0.0`. SDK v4 plugins are treated as incompatible until rebuilt.
|
||||
|
||||
The SDK SHALL keep the existing public UI extension shape, including `SettingsPageBase` and Avalonia `Control` based desktop components.
|
||||
|
||||
### Requirement: Launcher data location stability
|
||||
|
||||
Launcher data location configuration SHALL be read from a fixed bootstrap Launcher data directory so resolving the selected data root cannot recursively require resolving itself.
|
||||
|
||||
### Requirement: OOBE state compatibility
|
||||
|
||||
The Launcher SHALL read current OOBE state from the resolved `Launcher/state` directory and SHALL continue to migrate the legacy `.launcher/state/first_run_completed` marker.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- `dotnet build LanMountainDesktop.slnx -c Debug` completes with 0 errors.
|
||||
- `OobeStateServiceTests` pass.
|
||||
- Full `dotnet test LanMountainDesktop.slnx -c Debug` no longer aborts from `DataLocationResolver` recursion.
|
||||
- Plugin template defaults to SDK package version `5.0.0` and manifest `apiVersion` `5.0.0`.
|
||||
- Current developer documentation points to SDK v5 and Avalonia 12.
|
||||
18
.trae/specs/avalonia-12-migration/tasks.md
Normal file
18
.trae/specs/avalonia-12-migration/tasks.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Tasks
|
||||
|
||||
- [x] Centralize Avalonia 12 package versions in `Directory.Packages.props`.
|
||||
- [x] Move the host, Launcher, Plugin SDK, DesktopHost, Shared.Contracts, and Avalonia-facing projects onto central package versions.
|
||||
- [x] Replace third-party `WebView.Avalonia` usage with official `NativeWebView`.
|
||||
- [x] Configure WebView2 user data through `EnvironmentRequested`.
|
||||
- [x] Move FluentAvalonia usages to the FA3 control names and package baseline.
|
||||
- [x] Move FluentIcons usage to `FluentIcons.Avalonia` and remove the old `.Fluent` package.
|
||||
- [x] Update Plugin SDK package version and API baseline to `5.0.0`.
|
||||
- [x] Update plugin runtime shared assembly policy for Avalonia 12 / FluentAvalonia / FluentIcons / Material.
|
||||
- [x] Fix Avalonia 12 compile breaks in window chrome, binding plugin access, clipboard, bitmap copy, and icon source usage.
|
||||
- [x] Fix Launcher data location recursion by using a fixed bootstrap config path.
|
||||
- [x] Fix OOBE state tests and legacy marker compatibility.
|
||||
- [x] Update PluginTemplate defaults to SDK v5.
|
||||
- [x] Add SDK v5 migration documentation.
|
||||
- [x] Update current docs from SDK v4 / Avalonia 11 examples to SDK v5 / Avalonia 12.
|
||||
- [x] Run full solution tests and record any remaining non-upgrade failures.
|
||||
- [ ] Perform Windows manual smoke test for host, Launcher, settings, component editor, BrowserWidget, and WebView2 missing-runtime handling.
|
||||
42
.trae/specs/localization-fix/checklist.md
Normal file
42
.trae/specs/localization-fix/checklist.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 本地化修复 Checklist
|
||||
|
||||
## MainWindow 修复
|
||||
- [ ] `TaskbarProfileDisplayNameTextBlock.Text` 在中文下显示"用户"(或保持动态)
|
||||
- [ ] `TaskbarProfileSettingsActionTextBlock.Text` 在中文下显示"设置"
|
||||
- [ ] `TaskbarProfileDesktopEditActionTextBlock.Text` 在中文下显示"桌面编辑"
|
||||
- [ ] `TaskbarProfilePowerActionTextBlock.Text` 在中文下显示"电源"
|
||||
- [ ] `TaskbarPowerBackTextBlock.Text` 在中文下显示"返回"
|
||||
- [ ] `TaskbarPowerTitleTextBlock.Text` 在中文下显示"电源"
|
||||
- [ ] `PowerShutdownTextBlock.Text` 在中文下显示"关机"
|
||||
- [ ] `PowerRestartTextBlock.Text` 在中文下显示"重启"
|
||||
- [ ] `PowerLogoutTextBlock.Text` 在中文下显示"注销"
|
||||
- [ ] `PowerSleepTextBlock.Text` 在中文下显示"睡眠"
|
||||
- [ ] `PowerLockTextBlock.Text` 在中文下显示"锁定屏幕"
|
||||
- [ ] `ComponentLibraryTitleTextBlock.Text` 在中文下显示"桌面编辑"
|
||||
- [ ] `ComponentLibraryEmptyTextBlock.Text` 在中文下显示"左右滑动选择类别,点击进入,然后拖动组件到桌面放置。"
|
||||
- [ ] `ComponentLibraryBackTextBlock.Text` 在中文下显示"返回"
|
||||
- [ ] `ComponentLibraryCollapsedChipTextBlock.Text` 在中文下显示"桌面编辑"
|
||||
|
||||
## Launcher 修复
|
||||
- [ ] `SplashWindow` 在中文下显示中文启动文本
|
||||
- [ ] `DataLocationPromptWindow` 在中文下全部显示中文
|
||||
- [ ] `ErrorWindow` 在中文下全部显示中文
|
||||
- [ ] `LoadingDetailsWindow` 在中文下全部显示中文
|
||||
- [ ] `UpdateWindow` 在中文下显示中文标题
|
||||
|
||||
## 组件修复
|
||||
- [ ] `BrowserWidget` 在中文下显示"浏览器运行时不可用"
|
||||
- [ ] `WhiteboardWidget` 工具提示在中文下显示"笔"、"橡皮擦"、"清空"、"导出 SVG"
|
||||
- [ ] `HolidayCalendarWidget` 在中文下显示"节假日倒计时"、"天"
|
||||
- [ ] `BilibiliHotSearchWidget` 在中文下显示"热门话题"
|
||||
- [ ] `WallpaperSettingsPage` 自定义颜色 Tooltip 在中文下显示"自定义颜色"
|
||||
|
||||
## 资源文件
|
||||
- [ ] `zh-CN.json` 包含所有新增键值
|
||||
- [ ] `en-US.json` 包含所有新增键值
|
||||
- [ ] Launcher 本地化文件包含所有新增键值
|
||||
|
||||
## 构建与质量
|
||||
- [ ] `dotnet build LanMountainDesktop.slnx -c Debug` 编译通过,无错误
|
||||
- [ ] 无新增警告
|
||||
- [ ] 无遗漏的硬编码英文(通过 `grep -r 'Text="[a-zA-Z]'` 等检查)
|
||||
85
.trae/specs/localization-fix/spec.md
Normal file
85
.trae/specs/localization-fix/spec.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 本地化修复 Spec
|
||||
|
||||
## Why
|
||||
|
||||
- 项目在中文设置下,多处 UI 仍显示英文。
|
||||
- 主要问题集中在:
|
||||
1. `MainWindow.axaml` 中任务栏头像弹窗、电源菜单、组件库等文本硬编码为英文,且未被 `ApplyLocalization()` 覆盖。
|
||||
2. `LanMountainDesktop.Launcher` 的所有视图完全没有接入本地化系统。
|
||||
3. 部分组件(BrowserWidget、WhiteboardWidget、HolidayCalendarWidget 等)存在未覆盖的硬编码英文。
|
||||
4. 少量设置页面 Tooltip 硬编码英文。
|
||||
|
||||
## What Changes
|
||||
|
||||
### 1. MainWindow.axaml 硬编码修复
|
||||
将以下硬编码文本改为由 `ApplyLocalization()` 通过 `L()` 动态设置:
|
||||
- 任务栏头像弹窗:`User` → `power.user` / `Settings` → `settings.title` / `Edit Desktop` → `button.component_library` / `Power` → `power.title`
|
||||
- 电源菜单:`Back` → `common.back` / `Power` → `power.title` / `Shutdown` → `power.shutdown` / `Restart` → `power.restart` / `Log Out` → `power.logout` / `Sleep` → `power.sleep` / `Lock Screen` → `power.lock_screen`
|
||||
- 组件库:`Widgets` → `component_library.title` / `Back` → `common.back` / `No components.` → `component_library.empty`
|
||||
- 悬浮芯片:`Widgets` → `component_library.title`
|
||||
|
||||
### 2. Launcher 视图本地化
|
||||
为 `LanMountainDesktop.Launcher/Views/` 下的窗口引入独立本地化机制(复用 `LocalizationService` 或内嵌资源字典):
|
||||
- `SplashWindow.axaml`:`LanMountain Desktop`、`Initializing...`
|
||||
- `DataLocationPromptWindow.axaml`:全部文本
|
||||
- `ErrorWindow.axaml`:全部文本
|
||||
- `LoadingDetailsWindow.axaml`:全部文本
|
||||
- `UpdateWindow.axaml`:`Update`
|
||||
|
||||
### 3. 组件硬编码修复
|
||||
- `BrowserWidget.axaml`:`Browser runtime unavailable.` → 新增键 `browser.widget.unavailable`
|
||||
- `WhiteboardWidget.axaml`:`Pen` / `Eraser` / `Clear` / `Export SVG` → 新增键 `whiteboard.tool.pen` 等
|
||||
- `HolidayCalendarWidget.axaml`:`Holiday countdown` / `Days` → 新增键 `holiday.widget.title` / `holiday.widget.days`
|
||||
- `BilibiliHotSearchWidget.axaml`:`Trending Topic` → 新增键 `bilihot.widget.trending_topic`
|
||||
- `WallpaperSettingsPage.axaml`:`Custom color` Tooltip → 复用 `settings.wallpaper.custom_color_tooltip`
|
||||
|
||||
### 4. 本地化资源文件补充
|
||||
在 `zh-CN.json` 和 `en-US.json` 中补充上述新增键值。
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected code:
|
||||
- `LanMountainDesktop/Views/MainWindow.axaml`
|
||||
- `LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs`
|
||||
- `LanMountainDesktop.Launcher/Views/*.axaml`(多个文件)
|
||||
- `LanMountainDesktop/Views/Components/BrowserWidget.axaml`
|
||||
- `LanMountainDesktop/Views/Components/WhiteboardWidget.axaml`
|
||||
- `LanMountainDesktop/Views/Components/HolidayCalendarWidget.axaml`
|
||||
- `LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml`
|
||||
- `LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml`
|
||||
- `LanMountainDesktop/Localization/zh-CN.json`
|
||||
- `LanMountainDesktop/Localization/en-US.json`
|
||||
- Affected behavior:
|
||||
- 中文设置下上述位置将正确显示中文。
|
||||
- Launcher 各窗口将支持中英文切换。
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: MainWindow 任务栏弹窗与电源菜单本地化
|
||||
系统 SHALL 在 `ApplyLocalization()` 中覆盖任务栏头像弹窗和电源菜单的所有文本。
|
||||
|
||||
#### Scenario: 中文设置下打开任务栏弹窗
|
||||
- **WHEN** 语言设置为中文
|
||||
- **THEN** 弹窗中显示"设置"、"桌面编辑"、"电源"等中文文本
|
||||
- **AND THEN** 电源菜单中显示"返回"、"关机"、"重启"、"注销"、"睡眠"、"锁定屏幕"等中文文本
|
||||
|
||||
### Requirement: Launcher 窗口本地化
|
||||
系统 SHALL 让 Launcher 的所有窗口文本通过本地化服务获取。
|
||||
|
||||
#### Scenario: 中文设置下启动应用
|
||||
- **WHEN** 语言设置为中文
|
||||
- **THEN** SplashWindow 显示中文启动文本
|
||||
- **AND THEN** 数据位置选择、错误页、加载详情页等显示中文
|
||||
|
||||
### Requirement: 组件与设置页硬编码修复
|
||||
系统 SHALL 移除或覆盖所有组件和设置页中的英文硬编码文本。
|
||||
|
||||
#### Scenario: 中文设置下查看各组件
|
||||
- **WHEN** 语言设置为中文
|
||||
- **THEN** BrowserWidget 显示"浏览器运行时不可用"
|
||||
- **AND THEN** WhiteboardWidget 工具提示显示"笔"、"橡皮擦"、"清空"、"导出 SVG"
|
||||
- **AND THEN** HolidayCalendarWidget 显示"节假日倒计时"、"天"
|
||||
- **AND THEN** BilibiliHotSearchWidget 显示"热门话题"
|
||||
- **AND THEN** 壁纸设置页自定义颜色 Tooltip 显示"自定义颜色"
|
||||
39
.trae/specs/localization-fix/tasks.md
Normal file
39
.trae/specs/localization-fix/tasks.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 本地化修复 Tasks
|
||||
|
||||
## Task 1: MainWindow.axaml 硬编码文本移除与代码覆盖
|
||||
- [ ] 1.1 在 `MainWindow.axaml` 中,将任务栏头像弹窗的 `User`、`Settings`、`Edit Desktop`、`Power` 的 `Text` 属性改为空或绑定(保留 x:Name)
|
||||
- [ ] 1.2 在 `MainWindow.axaml` 中,将电源菜单的 `Back`、`Power`、`Shutdown`、`Restart`、`Log Out`、`Sleep`、`Lock Screen` 的 `Text` 属性改为空或绑定
|
||||
- [ ] 1.3 在 `MainWindow.axaml` 中,将组件库的 `Widgets`、`Back`、`No components.` 的 `Text` 属性改为空或绑定
|
||||
- [ ] 1.4 在 `MainWindow.axaml` 中,将悬浮芯片的 `Widgets` 的 `Text` 属性改为空或绑定
|
||||
- [ ] 1.5 在 `MainWindow.SettingsHardCut.Stubs.cs` 的 `ApplyLocalization()` 中补充上述所有控件的 `L()` 赋值
|
||||
|
||||
## Task 2: Launcher 视图本地化
|
||||
- [ ] 2.1 在 `LanMountainDesktop.Launcher` 中引入 `LocalizationService`(或共享主应用服务)
|
||||
- [ ] 2.2 为 Launcher 创建独立的 `Localization/` 目录和 `zh-CN.json` / `en-US.json`
|
||||
- [ ] 2.3 修改 `SplashWindow.axaml`:将 `LanMountain Desktop`、`Initializing...` 改为动态绑定
|
||||
- [ ] 2.4 修改 `DataLocationPromptWindow.axaml`:将所有文本改为动态绑定
|
||||
- [ ] 2.5 修改 `ErrorWindow.axaml`:将所有文本改为动态绑定
|
||||
- [ ] 2.6 修改 `LoadingDetailsWindow.axaml`:将所有文本改为动态绑定
|
||||
- [ ] 2.7 修改 `UpdateWindow.axaml`:将 `Update` 改为动态绑定
|
||||
- [ ] 2.8 在 Launcher 启动流程中初始化语言设置
|
||||
|
||||
## Task 3: 组件硬编码修复
|
||||
- [ ] 3.1 `BrowserWidget.axaml`:将 `Browser runtime unavailable.` 改为绑定,并在代码后置中通过 `L()` 设置
|
||||
- [ ] 3.2 `WhiteboardWidget.axaml`:将 `Pen`、`Eraser`、`Clear`、`Export SVG` Tooltip 改为绑定,并在代码后置中通过 `L()` 设置
|
||||
- [ ] 3.3 `HolidayCalendarWidget.axaml`:将 `Holiday countdown`、`Days` 改为绑定,并在代码后置中通过 `L()` 设置
|
||||
- [ ] 3.4 `BilibiliHotSearchWidget.axaml`:将 `Trending Topic` 改为绑定,并在代码后置中通过 `L()` 设置
|
||||
- [ ] 3.5 `WallpaperSettingsPage.axaml`:将 `Custom color` Tooltip 改为绑定到 `settings.wallpaper.custom_color_tooltip`
|
||||
|
||||
## Task 4: 本地化资源文件补充
|
||||
- [ ] 4.1 在 `zh-CN.json` 中补充以下键值:
|
||||
- `browser.widget.unavailable`
|
||||
- `whiteboard.tool.pen`、`whiteboard.tool.eraser`、`whiteboard.tool.clear`、`whiteboard.tool.export_svg`
|
||||
- `holiday.widget.title`、`holiday.widget.days`
|
||||
- `bilihot.widget.trending_topic`
|
||||
- `power.user`(或复用现有键)
|
||||
- [ ] 4.2 在 `en-US.json` 中补充上述键值的英文版本
|
||||
- [ ] 4.3 为 Launcher 创建独立的本地化 JSON 文件并填充中英文
|
||||
|
||||
## Task 5: 验证
|
||||
- [ ] 5.1 执行 `dotnet build LanMountainDesktop.slnx -c Debug` 确保编译通过
|
||||
- [ ] 5.2 检查是否有遗漏的硬编码英文(通过正则搜索)
|
||||
@@ -74,7 +74,7 @@ dotnet test LanMountainDesktop.slnx -c Debug
|
||||
- SDK 公共 API 以 `LanMountainDesktop.PluginSdk/` 为准
|
||||
- 共享契约以 `LanMountainDesktop.Shared.Contracts/` 为准
|
||||
- market 数据来源默认是兄弟仓库 `..\\LanAirApp`
|
||||
- 迁移或 breaking change 优先同步 `docs/PLUGIN_SDK_V4_MIGRATION.md`
|
||||
- 迁移或 breaking change 优先同步 `docs/PLUGIN_SDK_V5_MIGRATION.md`
|
||||
|
||||
### 设置与主题
|
||||
|
||||
@@ -91,6 +91,6 @@ dotnet test LanMountainDesktop.slnx -c Debug
|
||||
- 视觉规范:`docs/VISUAL_SPEC.md`
|
||||
- 圆角规范:`docs/CORNER_RADIUS_SPEC.md`
|
||||
- 生态边界:`docs/ECOSYSTEM_BOUNDARIES.md`
|
||||
- SDK v4 迁移:`docs/PLUGIN_SDK_V4_MIGRATION.md`
|
||||
- SDK v5 迁移:`docs/PLUGIN_SDK_V5_MIGRATION.md`
|
||||
|
||||
如果多个文档都提到同一件事,以 `docs/ai/DOC_SOURCES.md` 列出的权威来源为准。
|
||||
|
||||
42
Directory.Packages.props
Normal file
42
Directory.Packages.props
Normal file
@@ -0,0 +1,42 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="12.0.1" />
|
||||
<PackageVersion Include="Avalonia.Controls.WebView" Version="12.0.0" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="12.0.1" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.0.1" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.1" />
|
||||
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1" />
|
||||
<PackageVersion Include="ClassIsland.Markdown.Avalonia" Version="12.0.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||
<PackageVersion Include="dotnetCampus.Ipc" Version="2.0.0-alpha434" />
|
||||
<PackageVersion Include="DotNetCampus.AvaloniaInkCanvas" Version="1.0.1" />
|
||||
<PackageVersion Include="Downloader" Version="4.1.1" />
|
||||
<PackageVersion Include="FluentAvaloniaUI" Version="3.0.0-preview1" />
|
||||
<PackageVersion Include="FluentIcons.Avalonia" Version="2.1.325" />
|
||||
<PackageVersion Include="Material.Avalonia" Version="3.16.0" />
|
||||
<PackageVersion Include="MaterialColorUtilities" Version="0.3.0" />
|
||||
<PackageVersion Include="Material.Icons.Avalonia" Version="3.0.2" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="MudTools.OfficeInterop" Version="2.0.8" />
|
||||
<PackageVersion Include="MudTools.OfficeInterop.Excel" Version="2.0.8" />
|
||||
<PackageVersion Include="MudTools.OfficeInterop.PowerPoint" Version="2.0.8" />
|
||||
<PackageVersion Include="MudTools.OfficeInterop.Word" Version="2.0.8" />
|
||||
<PackageVersion Include="PortAudioSharp2" Version="1.0.6" />
|
||||
<PackageVersion Include="PostHog" Version="2.4.0" />
|
||||
<PackageVersion Include="Sentry" Version="4.0.0" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="10.0.0" />
|
||||
<PackageVersion Include="System.Runtime.WindowsRuntime" Version="4.7.0" />
|
||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.92.0" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
|
||||
<PackageVersion Include="log4net" Version="3.3.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -5,7 +5,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
<PackageReference Include="Avalonia" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||
|
||||
@@ -22,16 +22,14 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="2.5.0" />
|
||||
<PackageReference Include="FluentIcons.Avalonia" Version="1.1.250403001" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
|
||||
<PackageReference Include="Tmds.DBus.Protocol" Version="0.92.0" />
|
||||
<!-- Markdown.Avalonia 支持 (兼容 Avalonia 11.x) -->
|
||||
<PackageReference Include="Markdown.Avalonia" Version="11.0.3-a1" />
|
||||
<!-- MVVM 支持 -->
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Avalonia.Desktop" />
|
||||
<PackageReference Include="FluentAvaloniaUI" />
|
||||
<PackageReference Include="FluentIcons.Avalonia" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||
<PackageReference Include="ClassIsland.Markdown.Avalonia" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="Tmds.DBus.Protocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- 资源文件 -->
|
||||
|
||||
@@ -32,6 +32,11 @@ internal sealed class DataLocationResolver
|
||||
/// </summary>
|
||||
public string DefaultPortableDataPath => Path.Combine(_appRoot, "AppData");
|
||||
|
||||
private string ResolveBootstrapLauncherDataPath()
|
||||
{
|
||||
return Path.Combine(_defaultSystemDataPath, LauncherFolderName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否允许便携模式(应用目录是否可写)
|
||||
/// </summary>
|
||||
@@ -56,6 +61,11 @@ internal sealed class DataLocationResolver
|
||||
public string ResolveDataRoot()
|
||||
{
|
||||
var config = LoadConfig();
|
||||
return ResolveDataRoot(config);
|
||||
}
|
||||
|
||||
private string ResolveDataRoot(DataLocationConfig? config)
|
||||
{
|
||||
if (config is null)
|
||||
{
|
||||
return _defaultSystemDataPath;
|
||||
@@ -65,7 +75,7 @@ internal sealed class DataLocationResolver
|
||||
{
|
||||
var portablePath = !string.IsNullOrWhiteSpace(config.PortableDataPath)
|
||||
? config.PortableDataPath
|
||||
: _defaultSystemDataPath;
|
||||
: DefaultPortableDataPath;
|
||||
return Path.GetFullPath(portablePath);
|
||||
}
|
||||
|
||||
@@ -95,7 +105,7 @@ internal sealed class DataLocationResolver
|
||||
/// </summary>
|
||||
public string ResolveConfigPath()
|
||||
{
|
||||
return Path.Combine(ResolveLauncherDataPath(), ConfigFileName);
|
||||
return Path.Combine(ResolveBootstrapLauncherDataPath(), ConfigFileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -153,7 +163,7 @@ internal sealed class DataLocationResolver
|
||||
{
|
||||
try
|
||||
{
|
||||
var launcherPath = ResolveLauncherDataPath();
|
||||
var launcherPath = ResolveBootstrapLauncherDataPath();
|
||||
Directory.CreateDirectory(launcherPath);
|
||||
|
||||
var configPath = ResolveConfigPath();
|
||||
@@ -184,8 +194,9 @@ internal sealed class DataLocationResolver
|
||||
// 先创建目录结构
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ResolveLauncherDataPath());
|
||||
Directory.CreateDirectory(ResolveDesktopDataPath());
|
||||
var resolvedDataRoot = ResolveDataRoot(config);
|
||||
Directory.CreateDirectory(Path.Combine(resolvedDataRoot, LauncherFolderName));
|
||||
Directory.CreateDirectory(Path.Combine(resolvedDataRoot, DesktopFolderName));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ internal sealed class OobeStateService
|
||||
|
||||
private readonly string _stateDirectory;
|
||||
private readonly string _statePath;
|
||||
private readonly string _legacyStatePath;
|
||||
private readonly string _legacyMarkerPath;
|
||||
private readonly LauncherExecutionSnapshot _executionSnapshot;
|
||||
|
||||
@@ -25,7 +26,13 @@ internal sealed class OobeStateService
|
||||
: Path.GetFullPath(stateRootOverride);
|
||||
_stateDirectory = Path.Combine(stateRoot, "Launcher", "state");
|
||||
_statePath = Path.Combine(_stateDirectory, "oobe-state.json");
|
||||
_legacyMarkerPath = Path.Combine(_stateDirectory, "first_run_completed");
|
||||
|
||||
var legacyRoot = string.IsNullOrWhiteSpace(stateRootOverride)
|
||||
? Path.GetFullPath(appRoot)
|
||||
: Path.GetFullPath(stateRootOverride);
|
||||
var legacyStateDirectory = Path.Combine(legacyRoot, ".launcher", "state");
|
||||
_legacyStatePath = Path.Combine(legacyStateDirectory, "oobe-state.json");
|
||||
_legacyMarkerPath = Path.Combine(legacyStateDirectory, "first_run_completed");
|
||||
}
|
||||
|
||||
public OobeLaunchDecision Evaluate(CommandContext context)
|
||||
@@ -100,14 +107,12 @@ internal sealed class OobeStateService
|
||||
var migratedLegacyMarker = false;
|
||||
if (File.Exists(_statePath))
|
||||
{
|
||||
using var stream = File.OpenRead(_statePath);
|
||||
var state = JsonSerializer.Deserialize(stream, AppJsonContext.Default.OobeStateFile);
|
||||
if (state is null || state.SchemaVersion <= 0 || string.IsNullOrWhiteSpace(state.CompletedAtUtc))
|
||||
{
|
||||
return BuildUnavailableDecision(context, "OOBE state file is invalid.");
|
||||
}
|
||||
return EvaluateStateFile(context, _statePath, migratedLegacyState: false);
|
||||
}
|
||||
|
||||
return BuildDecision(context, OobeStateStatus.Completed, shouldShowOobe: false, migratedLegacyMarker: false);
|
||||
if (File.Exists(_legacyStatePath))
|
||||
{
|
||||
return EvaluateStateFile(context, _legacyStatePath, migratedLegacyState: false);
|
||||
}
|
||||
|
||||
if (File.Exists(_legacyMarkerPath))
|
||||
@@ -140,6 +145,18 @@ internal sealed class OobeStateService
|
||||
return result.Success;
|
||||
}
|
||||
|
||||
private OobeLaunchDecision EvaluateStateFile(CommandContext context, string statePath, bool migratedLegacyState)
|
||||
{
|
||||
using var stream = File.OpenRead(statePath);
|
||||
var state = JsonSerializer.Deserialize(stream, AppJsonContext.Default.OobeStateFile);
|
||||
if (state is null || state.SchemaVersion <= 0 || string.IsNullOrWhiteSpace(state.CompletedAtUtc))
|
||||
{
|
||||
return BuildUnavailableDecision(context, "OOBE state file is invalid.");
|
||||
}
|
||||
|
||||
return BuildDecision(context, OobeStateStatus.Completed, shouldShowOobe: false, migratedLegacyMarker: migratedLegacyState);
|
||||
}
|
||||
|
||||
private void TryDeleteLegacyMarker()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -9,8 +9,10 @@ namespace LanMountainDesktop.Launcher.Services;
|
||||
/// </summary>
|
||||
internal sealed class PluginInstallerService
|
||||
{
|
||||
private const string ManifestFileName = "manifest.json";
|
||||
private const string PackageFileExtension = ".lmdp";
|
||||
private const string ManifestFileName = "plugin.json";
|
||||
private const string LegacyManifestFileName = "manifest.json";
|
||||
private const string PackageFileExtension = ".laapp";
|
||||
private const string LegacyPackageFileExtension = ".lmdp";
|
||||
private const string RuntimeDirectoryName = "runtime";
|
||||
|
||||
private static readonly TimeSpan[] RetryDelays =
|
||||
@@ -114,14 +116,16 @@ internal sealed class PluginInstallerService
|
||||
public PluginManifest ReadManifestFromPackage(string packagePath)
|
||||
{
|
||||
using var archive = ZipFile.OpenRead(packagePath);
|
||||
var entries = archive.Entries
|
||||
.Where(entry => string.Equals(entry.Name, ManifestFileName, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
var entries = FindManifestEntries(archive, ManifestFileName);
|
||||
if (entries.Length == 0)
|
||||
{
|
||||
entries = FindManifestEntries(archive, LegacyManifestFileName);
|
||||
}
|
||||
|
||||
if (entries.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Plugin package '{packagePath}' does not contain '{ManifestFileName}'.");
|
||||
$"Plugin package '{packagePath}' does not contain '{ManifestFileName}' or '{LegacyManifestFileName}'.");
|
||||
}
|
||||
|
||||
if (entries.Length > 1)
|
||||
@@ -141,6 +145,13 @@ internal sealed class PluginInstallerService
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private static ZipArchiveEntry[] FindManifestEntries(ZipArchive archive, string manifestFileName)
|
||||
{
|
||||
return archive.Entries
|
||||
.Where(entry => string.Equals(entry.Name, manifestFileName, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId, string destinationPath, string stagingPath)
|
||||
{
|
||||
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), RuntimeDirectoryName));
|
||||
@@ -148,8 +159,11 @@ internal sealed class PluginInstallerService
|
||||
Directory.CreateDirectory(pendingDeletionDir);
|
||||
|
||||
foreach (var existingPackagePath in Directory
|
||||
.EnumerateFiles(pluginsDirectory, "*" + PackageFileExtension, SearchOption.AllDirectories)
|
||||
.EnumerateFiles(pluginsDirectory, "*", SearchOption.AllDirectories)
|
||||
.Select(Path.GetFullPath)
|
||||
.Where(path =>
|
||||
path.EndsWith(PackageFileExtension, StringComparison.OrdinalIgnoreCase) ||
|
||||
path.EndsWith(LegacyPackageFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(path => !path.StartsWith(runtimeRootDirectory, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
try
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="520"
|
||||
d:DesignHeight="480"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.DataLocationPromptWindow"
|
||||
x:DataType="views:DataLocationPromptWindow"
|
||||
Title="选择数据保存位置"
|
||||
Title="Choose Data Location"
|
||||
Width="520"
|
||||
Height="480"
|
||||
CanResize="False"
|
||||
@@ -24,12 +24,13 @@
|
||||
</Grid.RenderTransform>
|
||||
<Grid Margin="36" RowDefinitions="Auto,*,Auto">
|
||||
<StackPanel Grid.Row="0" Spacing="8" Margin="0,0,0,20">
|
||||
<TextBlock Text="选择数据保存位置"
|
||||
<TextBlock Text="Choose Data Location"
|
||||
FontSize="22"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="决定将应用数据保存在哪里,可随时在设置中更改"
|
||||
<TextBlock Text="Choose where launcher and desktop data should be stored. You can change this later in settings."
|
||||
FontSize="13"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -41,15 +42,15 @@
|
||||
IsVisible="False">
|
||||
<StackPanel Spacing="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<ui:SymbolIcon Symbol="Important"
|
||||
<fi:SymbolIcon Symbol="Important"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
|
||||
<TextBlock Text="无法保存到应用目录"
|
||||
<TextBlock Text="App folder is not writable"
|
||||
FontWeight="SemiBold"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="当前安装目录需要管理员权限才能写入,数据将自动保存到系统用户目录。"
|
||||
<TextBlock Text="The current install directory requires elevated permissions. Data will be stored in the system user profile instead."
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
|
||||
@@ -70,11 +71,11 @@
|
||||
GroupName="DataLocation"
|
||||
IsChecked="True" />
|
||||
<StackPanel Grid.Column="1" Spacing="4">
|
||||
<TextBlock Text="保存在系统用户目录(推荐)"
|
||||
<TextBlock Text="Store in the system user profile (Recommended)"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="数据与当前 Windows 用户绑定,重装应用或更新后数据不会丢失"
|
||||
<TextBlock Text="Data stays tied to the current Windows user and remains intact across app reinstalls and updates."
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -101,11 +102,11 @@
|
||||
GroupName="DataLocation"
|
||||
IsEnabled="False" />
|
||||
<StackPanel Grid.Column="1" Spacing="4">
|
||||
<TextBlock Text="保存在应用安装目录"
|
||||
<TextBlock Text="Store next to the app"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
<TextBlock Text="便于携带,可随应用文件夹整体移动到其他电脑"
|
||||
<TextBlock Text="Useful for portable installs. The whole app folder can be moved to another machine together with its data."
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
@@ -124,7 +125,7 @@
|
||||
Padding="12,10"
|
||||
IsVisible="False">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<ui:SymbolIcon Symbol="Message"
|
||||
<fi:SymbolIcon Symbol="Info"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource SystemFillColorSuccessBrush}" />
|
||||
<TextBlock x:Name="MigrationInfoText"
|
||||
@@ -141,11 +142,11 @@
|
||||
Spacing="10"
|
||||
Margin="0,20,0,0">
|
||||
<Button x:Name="CancelButton"
|
||||
Content="取消"
|
||||
Content="Cancel"
|
||||
Theme="{DynamicResource ButtonTheme}"
|
||||
IsVisible="False" />
|
||||
<Button x:Name="ConfirmButton"
|
||||
Content="确认"
|
||||
Content="Confirm"
|
||||
Theme="{DynamicResource AccentButtonTheme}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -48,14 +48,12 @@ internal partial class DataLocationPromptWindow : Window
|
||||
|
||||
if (systemRadio is not null)
|
||||
{
|
||||
systemRadio.Checked += OnSelectionChanged;
|
||||
systemRadio.Unchecked += OnSelectionChanged;
|
||||
systemRadio.IsCheckedChanged += OnSelectionChanged;
|
||||
}
|
||||
|
||||
if (portableRadio is not null)
|
||||
{
|
||||
portableRadio.Checked += OnSelectionChanged;
|
||||
portableRadio.Unchecked += OnSelectionChanged;
|
||||
portableRadio.IsCheckedChanged += OnSelectionChanged;
|
||||
}
|
||||
|
||||
if (confirmButton is not null)
|
||||
@@ -108,7 +106,7 @@ internal partial class DataLocationPromptWindow : Window
|
||||
|
||||
if (migrationInfoText is not null && hasExistingData)
|
||||
{
|
||||
migrationInfoText.Text = "检测到系统用户目录已有应用数据。如果选择保存在应用安装目录,将自动迁移现有数据。";
|
||||
migrationInfoText.Text = "Existing system data was detected. Choosing portable mode will migrate the current data automatically.";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
CanResize="False"
|
||||
ShowInTaskbar="False"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
SystemDecorations="None"
|
||||
WindowDecorations="None"
|
||||
Background="#0B0B0B"
|
||||
TransparencyLevelHint="None"
|
||||
Icon="/Assets/logo.ico">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
Height="320"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
SystemDecorations="None"
|
||||
WindowDecorations="None"
|
||||
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
|
||||
TransparencyLevelHint="None"
|
||||
Icon="/Assets/logo.ico">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>1.0.0</Version>
|
||||
<Version>5.0.0</Version>
|
||||
<PackageId>LanMountainDesktop.PluginIsolation.Contracts</PackageId>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Authors>LanMountainDesktop</Authors>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>1.0.0</Version>
|
||||
<Version>5.0.0</Version>
|
||||
<PackageId>LanMountainDesktop.PluginIsolation.Ipc</PackageId>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Authors>LanMountainDesktop</Authors>
|
||||
@@ -17,7 +17,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="dotnetCampus.Ipc" Version="2.0.0-alpha434" />
|
||||
<PackageReference Include="dotnetCampus.Ipc" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginIsolation.Contracts\LanMountainDesktop.PluginIsolation.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>4.0.2</Version>
|
||||
<Version>5.0.0</Version>
|
||||
<PackageId>LanMountainDesktop.PluginSdk</PackageId>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Authors>LanMountainDesktop</Authors>
|
||||
@@ -19,13 +19,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="_build_verify_*\**\*.cs" />
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="2.5.0" ExcludeAssets="runtime" />
|
||||
<PackageReference Include="FluentIcons.Avalonia" Version="2.0.320" ExcludeAssets="runtime" />
|
||||
<PackageReference Include="FluentIcons.Avalonia.Fluent" Version="2.0.320" ExcludeAssets="runtime" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="dotnetCampus.Ipc" Version="2.0.0-alpha434" />
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="FluentAvaloniaUI" ExcludeAssets="runtime" />
|
||||
<PackageReference Include="FluentIcons.Avalonia" ExcludeAssets="runtime" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="dotnetCampus.Ipc" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.PluginIsolation.Contracts\LanMountainDesktop.PluginIsolation.Contracts.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.IPC\LanMountainDesktop.Shared.IPC.csproj" />
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public static class PluginSdkInfo
|
||||
{
|
||||
public const string ApiVersion = "4.0.2";
|
||||
public const string ApiVersion = "5.0.0";
|
||||
public const string ManifestFileName = "plugin.json";
|
||||
public const string PackageFileExtension = ".laapp";
|
||||
public const string DataDirectoryName = "Data";
|
||||
|
||||
@@ -16,7 +16,7 @@ Official SDK package for LanMountainDesktop plugins.
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="4.0.1" />
|
||||
<PackageReference Include="LanMountainDesktop.PluginSdk" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
Official `dotnet new` template package for LanMountainDesktop plugins.
|
||||
|
||||
## Baseline
|
||||
|
||||
- Target framework: `net10.0`
|
||||
- Plugin SDK: `LanMountainDesktop.PluginSdk` `5.0.0`
|
||||
- Manifest: `plugin.json`
|
||||
- Package: `.laapp`
|
||||
- Runtime mode: `in-proc`
|
||||
|
||||
## Install
|
||||
|
||||
```powershell
|
||||
@@ -15,3 +23,19 @@ dotnet new lmd-plugin -n YourPluginName
|
||||
```
|
||||
|
||||
The generated project references `LanMountainDesktop.PluginSdk` and produces a `.laapp` package automatically when built.
|
||||
|
||||
## Package contract
|
||||
|
||||
Every plugin package must contain:
|
||||
|
||||
- `plugin.json`
|
||||
- the entrance assembly declared by `entranceAssembly`
|
||||
- the `.deps.json` next to the entrance assembly
|
||||
|
||||
Optional package content:
|
||||
|
||||
- `Localization/*.json`
|
||||
- plugin assets and other managed dependencies
|
||||
- `airappmarket-entry.template.json` in the repository root for market publishing
|
||||
|
||||
Market publishing uses `market-manifest.json` with `schemaVersion`, `manifest`, `compatibility`, `repository`, `publication.packageSources`, and `capabilities`.
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"pluginSdkVersion": {
|
||||
"type": "parameter",
|
||||
"datatype": "text",
|
||||
"defaultValue": "4.0.2",
|
||||
"defaultValue": "5.0.0",
|
||||
"description": "LanMountainDesktop.PluginSdk package version.",
|
||||
"replaces": "__PLUGIN_SDK_VERSION__"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "__PLUGIN_DESCRIPTION__",
|
||||
"author": "__PLUGIN_AUTHOR__",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "4.0.2",
|
||||
"apiVersion": "5.0.0",
|
||||
"entranceAssembly": "LanMountainDesktop.PluginTemplate.dll",
|
||||
"sharedContracts": [],
|
||||
"runtime": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>0.0.0-dev</Version>
|
||||
<Version>5.0.0</Version>
|
||||
<PackageId>LanMountainDesktop.Shared.Contracts</PackageId>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Authors>LanMountainDesktop</Authors>
|
||||
@@ -17,7 +17,7 @@
|
||||
<Copyright>Copyright (c) LanMountainDesktop Contributors</Copyright>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
<PackageReference Include="Avalonia" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>1.0.0</Version>
|
||||
<Version>5.0.0</Version>
|
||||
<PackageId>LanMountainDesktop.Shared.IPC</PackageId>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Authors>LanMountainDesktop</Authors>
|
||||
@@ -17,8 +17,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="dotnetCampus.Ipc" Version="2.0.0-alpha434" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="dotnetCampus.Ipc" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -10,11 +10,10 @@ public sealed class ComponentLibraryCollapseStateTests
|
||||
public void CreateExpanded_InitializesExpandedStateAndHidesChip()
|
||||
{
|
||||
var margin = new Thickness(24, 24, 24, 100);
|
||||
var state = ComponentLibraryCollapseState.CreateExpanded(margin, 0.75);
|
||||
var state = ComponentLibraryCollapseState.CreateExpanded(margin);
|
||||
|
||||
Assert.Equal(ComponentLibraryCollapseVisualState.Expanded, state.VisualState);
|
||||
Assert.Equal(margin, state.ExpandedMargin);
|
||||
Assert.Equal(0.75, state.ExpandedOpacity, 3);
|
||||
Assert.False(state.IsChipVisible);
|
||||
}
|
||||
|
||||
@@ -22,7 +21,7 @@ public sealed class ComponentLibraryCollapseStateTests
|
||||
public void WithVisualState_PreservesStableExpandedSnapshotAcrossTransitions()
|
||||
{
|
||||
var margin = new Thickness(20, 18, 20, 96);
|
||||
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin, 1);
|
||||
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin);
|
||||
|
||||
var collapsing = expanded.WithVisualState(ComponentLibraryCollapseVisualState.Collapsing, isChipVisible: true);
|
||||
var collapsed = collapsing.WithVisualState(ComponentLibraryCollapseVisualState.Collapsed, isChipVisible: true);
|
||||
@@ -36,24 +35,19 @@ public sealed class ComponentLibraryCollapseStateTests
|
||||
Assert.Equal(margin, collapsed.ExpandedMargin);
|
||||
Assert.Equal(margin, restoring.ExpandedMargin);
|
||||
|
||||
Assert.Equal(1, collapsing.ExpandedOpacity, 3);
|
||||
Assert.Equal(1, collapsed.ExpandedOpacity, 3);
|
||||
Assert.Equal(1, restoring.ExpandedOpacity, 3);
|
||||
|
||||
Assert.True(collapsing.IsChipVisible);
|
||||
Assert.True(collapsed.IsChipVisible);
|
||||
Assert.False(restoring.IsChipVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateExpanded_ProducesRestorableSnapshotEvenWhenOriginalOpacityIsLow()
|
||||
public void CreateExpanded_DoesNotCaptureTransientOpacityAsRestorableState()
|
||||
{
|
||||
var margin = new Thickness(18, 22, 18, 88);
|
||||
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin, 0.15);
|
||||
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin);
|
||||
var restored = expanded.WithVisualState(ComponentLibraryCollapseVisualState.Expanded, isChipVisible: false);
|
||||
|
||||
Assert.Equal(margin, restored.ExpandedMargin);
|
||||
Assert.Equal(0.15, restored.ExpandedOpacity, 3);
|
||||
Assert.Equal(ComponentLibraryCollapseVisualState.Expanded, restored.VisualState);
|
||||
Assert.False(restored.IsChipVisible);
|
||||
}
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using LanMountainDesktop.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class ComponentPreviewImageServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task QueueGenerationAsync_ExecutesWorkSeriallyAcrossKeys()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var executionOrder = new List<string>();
|
||||
var activeCount = 0;
|
||||
var maxActiveCount = 0;
|
||||
|
||||
Task<ComponentPreviewImageEntry> Queue(string componentTypeId)
|
||||
{
|
||||
var key = ComponentPreviewKey.ForComponentType(componentTypeId, widthCells: 2, heightCells: 2);
|
||||
return service.QueueGenerationAsync(
|
||||
key,
|
||||
visualSignature: $"sig:{componentTypeId}",
|
||||
async _ =>
|
||||
{
|
||||
var activeNow = Interlocked.Increment(ref activeCount);
|
||||
maxActiveCount = Math.Max(maxActiveCount, activeNow);
|
||||
lock (executionOrder)
|
||||
{
|
||||
executionOrder.Add(componentTypeId);
|
||||
}
|
||||
|
||||
await Task.Delay(40);
|
||||
Interlocked.Decrement(ref activeCount);
|
||||
return CreateImage();
|
||||
});
|
||||
}
|
||||
|
||||
var first = Queue("Clock");
|
||||
var second = Queue("Weather");
|
||||
var third = Queue("Calendar");
|
||||
|
||||
await Task.WhenAll(first, second, third);
|
||||
|
||||
Assert.Equal(1, maxActiveCount);
|
||||
Assert.Equal(["Clock", "Weather", "Calendar"], executionOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueueGenerationAsync_DeduplicatesConcurrentRequestsForSameKey()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var generationCount = 0;
|
||||
var bitmap = CreateImage();
|
||||
var completion = new TaskCompletionSource<IImage?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Task<IImage?> Generation(CancellationToken _)
|
||||
{
|
||||
Interlocked.Increment(ref generationCount);
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
var first = service.QueueGenerationAsync(key, "clock-sig", Generation);
|
||||
var second = service.QueueGenerationAsync(key, "clock-sig", Generation);
|
||||
|
||||
Assert.Same(first, second);
|
||||
|
||||
completion.SetResult(bitmap);
|
||||
var entry = await first;
|
||||
|
||||
Assert.Equal(1, generationCount);
|
||||
Assert.Equal(ComponentPreviewImageState.Ready, entry.State);
|
||||
Assert.Same(bitmap, entry.Bitmap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Invalidate_ResetsSingleKeyToPending()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var image = CreateDisposableImage();
|
||||
var stored = service.Store(key, image, "clock-sig");
|
||||
var previousRevision = stored.Revision;
|
||||
|
||||
var result = service.Invalidate(key);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(ComponentPreviewImageState.Pending, stored.State);
|
||||
Assert.Null(stored.Bitmap);
|
||||
Assert.True(image.IsDisposed);
|
||||
Assert.True(stored.Revision > previousRevision);
|
||||
Assert.Equal("clock-sig", stored.VisualSignature);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovePlacementPreviews_RemovesOnlyMatchingPlacementEntries()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
|
||||
var removedClock = ComponentPreviewKey.ForPlacementInstance("Clock", "desk-1", widthCells: 2, heightCells: 2);
|
||||
var removedWeather = ComponentPreviewKey.ForPlacementInstance("Weather", "desk-1", widthCells: 4, heightCells: 2);
|
||||
var keptPlacement = ComponentPreviewKey.ForPlacementInstance("Clock", "desk-2", widthCells: 2, heightCells: 2);
|
||||
var keptType = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var removedClockImage = CreateDisposableImage();
|
||||
var removedWeatherImage = CreateDisposableImage();
|
||||
var keptPlacementImage = CreateDisposableImage();
|
||||
var keptTypeImage = CreateDisposableImage();
|
||||
|
||||
service.Store(removedClock, removedClockImage, "sig-a");
|
||||
service.Store(removedWeather, removedWeatherImage, "sig-b");
|
||||
service.Store(keptPlacement, keptPlacementImage, "sig-c");
|
||||
service.Store(keptType, keptTypeImage, "sig-d");
|
||||
|
||||
var removedCount = service.RemovePlacementPreviews("desk-1");
|
||||
|
||||
Assert.Equal(2, removedCount);
|
||||
Assert.False(service.TryGetEntry(removedClock, out _));
|
||||
Assert.False(service.TryGetEntry(removedWeather, out _));
|
||||
Assert.True(service.TryGetEntry(keptPlacement, out _));
|
||||
Assert.True(service.TryGetEntry(keptType, out _));
|
||||
Assert.True(removedClockImage.IsDisposed);
|
||||
Assert.True(removedWeatherImage.IsDisposed);
|
||||
Assert.False(keptPlacementImage.IsDisposed);
|
||||
Assert.False(keptTypeImage.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidateVisualSignature_InvalidatesEveryMatchingEntry()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
const string matchingSignature = "shared-sig";
|
||||
const string otherSignature = "other-sig";
|
||||
|
||||
var first = service.Store(
|
||||
ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2),
|
||||
CreateImage(),
|
||||
matchingSignature);
|
||||
var second = service.Store(
|
||||
ComponentPreviewKey.ForPlacementInstance("Clock", "desk-1", widthCells: 2, heightCells: 2),
|
||||
CreateImage(),
|
||||
matchingSignature);
|
||||
var third = service.Store(
|
||||
ComponentPreviewKey.ForComponentType("Weather", widthCells: 2, heightCells: 1),
|
||||
CreateImage(),
|
||||
otherSignature);
|
||||
|
||||
var invalidatedCount = service.InvalidateVisualSignature(matchingSignature);
|
||||
|
||||
Assert.Equal(2, invalidatedCount);
|
||||
Assert.Equal(ComponentPreviewImageState.Pending, first.State);
|
||||
Assert.Equal(ComponentPreviewImageState.Pending, second.State);
|
||||
Assert.Null(first.Bitmap);
|
||||
Assert.Null(second.Bitmap);
|
||||
Assert.Equal(ComponentPreviewImageState.Ready, third.State);
|
||||
Assert.NotNull(third.Bitmap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Store_ReplacingBitmap_DisposesPreviousBitmap_WhenInstanceChanges()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var first = CreateDisposableImage();
|
||||
var second = CreateDisposableImage();
|
||||
|
||||
service.Store(key, first, "sig-a");
|
||||
service.Store(key, second, "sig-b");
|
||||
|
||||
Assert.True(first.IsDisposed);
|
||||
Assert.False(second.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Store_ReplacingBitmap_DoesNotDispose_WhenSameInstanceReused()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var image = CreateDisposableImage();
|
||||
|
||||
service.Store(key, image, "sig-a");
|
||||
service.Store(key, image, "sig-b");
|
||||
|
||||
Assert.False(image.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoreFailure_DisposesExistingBitmap()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var image = CreateDisposableImage();
|
||||
|
||||
service.Store(key, image, "sig-a");
|
||||
var entry = service.StoreFailure(key, "sig-a", "failed");
|
||||
|
||||
Assert.True(image.IsDisposed);
|
||||
Assert.Equal(ComponentPreviewImageState.Failed, entry.State);
|
||||
Assert.Null(entry.Bitmap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueueGenerationAsync_DisposesStaleGeneratedBitmap_WhenEntryWasInvalidated()
|
||||
{
|
||||
var service = new ComponentPreviewImageService();
|
||||
var key = ComponentPreviewKey.ForComponentType("Clock", widthCells: 2, heightCells: 2);
|
||||
var completion = new TaskCompletionSource<IImage?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var stale = CreateDisposableImage();
|
||||
|
||||
var generationTask = service.QueueGenerationAsync(key, "sig-a", _ => completion.Task);
|
||||
_ = service.Invalidate(key);
|
||||
completion.SetResult(stale);
|
||||
var entry = await generationTask;
|
||||
|
||||
Assert.True(stale.IsDisposed);
|
||||
Assert.Equal(ComponentPreviewImageState.Pending, entry.State);
|
||||
Assert.Null(entry.Bitmap);
|
||||
}
|
||||
|
||||
private static IImage CreateImage() => new TestImage();
|
||||
private static DisposableTestImage CreateDisposableImage() => new();
|
||||
|
||||
private sealed class TestImage : IImage
|
||||
{
|
||||
public Size Size => new(1, 1);
|
||||
|
||||
public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
|
||||
{
|
||||
_ = context;
|
||||
_ = sourceRect;
|
||||
_ = destRect;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DisposableTestImage : IImage, IDisposable
|
||||
{
|
||||
public Size Size => new(1, 1);
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
public void Draw(DrawingContext context, Rect sourceRect, Rect destRect)
|
||||
{
|
||||
_ = context;
|
||||
_ = sourceRect;
|
||||
_ = destRect;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
using Xunit;
|
||||
@@ -34,10 +33,14 @@ public sealed class ComponentSettingsServiceTests
|
||||
Assert.Equal("Sweep", snapshot.DesktopClockSecondHandMode);
|
||||
Assert.Single(snapshot.ImportedClassSchedules);
|
||||
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(sandbox.SettingsPath));
|
||||
Assert.True(document.RootElement.TryGetProperty("defaultSettings", out var defaultSettings));
|
||||
Assert.Equal("Sweep", defaultSettings.GetProperty("desktopClockSecondHandMode").GetString());
|
||||
Assert.False(document.RootElement.TryGetProperty("DesktopClockSecondHandMode", out _));
|
||||
Assert.True(File.Exists(sandbox.DatabasePath));
|
||||
Assert.False(File.Exists(sandbox.SettingsPath));
|
||||
Assert.True(File.Exists(sandbox.SettingsBackupPath));
|
||||
|
||||
ComponentSettingsService.ResetCacheForTests();
|
||||
var reloadedService = sandbox.CreateService();
|
||||
var reloaded = reloadedService.Load();
|
||||
Assert.Equal("Sweep", reloaded.DesktopClockSecondHandMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -72,11 +75,16 @@ public sealed class ComponentSettingsServiceTests
|
||||
Assert.Equal("Sweep", snapshot.DesktopClockSecondHandMode);
|
||||
Assert.True(pluginSettings.SampleFlag);
|
||||
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(sandbox.SettingsPath));
|
||||
Assert.True(document.RootElement.TryGetProperty("instanceSettings", out var instanceSettings));
|
||||
Assert.True(instanceSettings.TryGetProperty("DesktopClock::clock-2x2", out var clockSettings));
|
||||
Assert.Equal("Sweep", clockSettings.GetProperty("desktopClockSecondHandMode").GetString());
|
||||
Assert.False(document.RootElement.TryGetProperty("InstanceSettings", out _));
|
||||
Assert.True(File.Exists(sandbox.DatabasePath));
|
||||
Assert.False(File.Exists(sandbox.SettingsPath));
|
||||
Assert.True(File.Exists(sandbox.SettingsBackupPath));
|
||||
|
||||
ComponentSettingsService.ResetCacheForTests();
|
||||
var reloadedService = sandbox.CreateService();
|
||||
var reloadedSnapshot = reloadedService.LoadForComponent("DesktopClock", "clock-2x2");
|
||||
var reloadedPluginSettings = reloadedService.LoadPluginSettings<SamplePluginSettings>("DesktopClock", "clock-2x2");
|
||||
Assert.Equal("Sweep", reloadedSnapshot.DesktopClockSecondHandMode);
|
||||
Assert.True(reloadedPluginSettings.SampleFlag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -132,12 +140,7 @@ public sealed class ComponentSettingsServiceTests
|
||||
Assert.True(pluginSettings.SampleFlag);
|
||||
Assert.Equal("schedule-settings", pluginSettings.Title);
|
||||
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(sandbox.SettingsPath));
|
||||
Assert.True(document.RootElement.TryGetProperty("instanceSettings", out var instanceSettings));
|
||||
Assert.True(instanceSettings.TryGetProperty("DesktopClock::clock-2x2", out _));
|
||||
Assert.True(instanceSettings.TryGetProperty("DesktopClassSchedule::class-schedule-2x2", out _));
|
||||
Assert.True(document.RootElement.TryGetProperty("pluginSettings", out var pluginSettingsNode));
|
||||
Assert.True(pluginSettingsNode.TryGetProperty("DesktopClassSchedule::class-schedule-2x2", out _));
|
||||
Assert.True(File.Exists(sandbox.DatabasePath));
|
||||
}
|
||||
|
||||
private sealed class ComponentSettingsSandbox : IDisposable
|
||||
@@ -155,6 +158,10 @@ public sealed class ComponentSettingsServiceTests
|
||||
|
||||
public string SettingsPath => Path.Combine(_directoryPath, "component-settings.json");
|
||||
|
||||
public string SettingsBackupPath => $"{SettingsPath}.migrated.bak";
|
||||
|
||||
public string DatabasePath => Path.Combine(_directoryPath, "component-state.db");
|
||||
|
||||
public ComponentSettingsService CreateService()
|
||||
{
|
||||
return new ComponentSettingsService(_directoryPath);
|
||||
|
||||
135
LanMountainDesktop.Tests/DesktopComponentRenderModeTests.cs
Normal file
135
LanMountainDesktop.Tests/DesktopComponentRenderModeTests.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class DesktopComponentRenderModeTests
|
||||
{
|
||||
private const string ComponentId = "RenderModeProbe";
|
||||
|
||||
[Fact]
|
||||
public void DescriptorCreateControl_DefaultsToLiveRenderMode()
|
||||
{
|
||||
var descriptor = CreateDescriptor();
|
||||
var control = (ProbeControl)descriptor.CreateControl(
|
||||
cellSize: 64,
|
||||
CreateTimeZoneService(),
|
||||
CreateWeatherInfoService(),
|
||||
new RecommendationDataService(),
|
||||
new CalculatorDataService(),
|
||||
CreateSettingsFacade(),
|
||||
placementId: "desktop-placement");
|
||||
|
||||
Assert.Equal(DesktopComponentRenderMode.Live, control.RuntimeContext?.RenderMode);
|
||||
Assert.Equal("desktop-placement", control.RuntimeContext?.PlacementId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DescriptorCreateControl_CanCreateLibraryPreviewRenderModeWithoutPlacement()
|
||||
{
|
||||
var descriptor = CreateDescriptor();
|
||||
var control = (ProbeControl)descriptor.CreateControl(
|
||||
cellSize: 64,
|
||||
CreateTimeZoneService(),
|
||||
CreateWeatherInfoService(),
|
||||
new RecommendationDataService(),
|
||||
new CalculatorDataService(),
|
||||
CreateSettingsFacade(),
|
||||
placementId: null,
|
||||
renderMode: DesktopComponentRenderMode.LibraryPreview);
|
||||
|
||||
Assert.Equal(DesktopComponentRenderMode.LibraryPreview, control.RuntimeContext?.RenderMode);
|
||||
Assert.Null(control.RuntimeContext?.PlacementId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComponentLibraryService_CreatesLibraryPreviewRenderMode()
|
||||
{
|
||||
var service = new ComponentLibraryService(
|
||||
CreateComponentRegistry(),
|
||||
CreateRuntimeRegistry());
|
||||
|
||||
var created = service.TryCreateControl(
|
||||
ComponentId,
|
||||
new ComponentLibraryCreateContext(
|
||||
64,
|
||||
CreateTimeZoneService(),
|
||||
CreateWeatherInfoService(),
|
||||
new RecommendationDataService(),
|
||||
new CalculatorDataService(),
|
||||
CreateSettingsFacade(),
|
||||
PlacementId: null,
|
||||
RenderMode: DesktopComponentRenderMode.LibraryPreview),
|
||||
out var control,
|
||||
out var exception);
|
||||
|
||||
Assert.True(created, exception?.ToString());
|
||||
var probe = Assert.IsType<ProbeControl>(control);
|
||||
Assert.Equal(DesktopComponentRenderMode.LibraryPreview, probe.RuntimeContext?.RenderMode);
|
||||
Assert.Null(probe.RuntimeContext?.PlacementId);
|
||||
}
|
||||
|
||||
private static DesktopComponentRuntimeDescriptor CreateDescriptor()
|
||||
{
|
||||
Assert.True(CreateRuntimeRegistry().TryGetDescriptor(ComponentId, out var descriptor));
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
private static DesktopComponentRuntimeRegistry CreateRuntimeRegistry()
|
||||
{
|
||||
return new DesktopComponentRuntimeRegistry(
|
||||
CreateComponentRegistry(),
|
||||
[
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
ComponentId,
|
||||
displayNameLocalizationKey: null,
|
||||
_ => new ProbeControl(),
|
||||
cornerRadiusResolver: (System.Func<double, double>?)null)
|
||||
]);
|
||||
}
|
||||
|
||||
private static ComponentRegistry CreateComponentRegistry()
|
||||
{
|
||||
return new ComponentRegistry(
|
||||
[
|
||||
new DesktopComponentDefinition(
|
||||
ComponentId,
|
||||
"Render Mode Probe",
|
||||
"Apps",
|
||||
"Test",
|
||||
MinWidthCells: 1,
|
||||
MinHeightCells: 1,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true)
|
||||
]);
|
||||
}
|
||||
|
||||
private static ISettingsFacadeService CreateSettingsFacade()
|
||||
{
|
||||
return HostSettingsFacadeProvider.GetOrCreate();
|
||||
}
|
||||
|
||||
private static TimeZoneService CreateTimeZoneService()
|
||||
{
|
||||
return CreateSettingsFacade().Region.GetTimeZoneService();
|
||||
}
|
||||
|
||||
private static IWeatherInfoService CreateWeatherInfoService()
|
||||
{
|
||||
return CreateSettingsFacade().Weather.GetWeatherInfoService();
|
||||
}
|
||||
|
||||
private sealed class ProbeControl : Control, IComponentRuntimeContextAware
|
||||
{
|
||||
public DesktopComponentRuntimeContext? RuntimeContext { get; private set; }
|
||||
|
||||
public void SetComponentRuntimeContext(DesktopComponentRuntimeContext context)
|
||||
{
|
||||
RuntimeContext = context;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -118,7 +118,7 @@ public sealed class OobeStateServiceTests : IDisposable
|
||||
executionSnapshot: executionSnapshot ?? new LauncherExecutionSnapshot(false, "tester", "S-1-5-test"));
|
||||
}
|
||||
|
||||
private string GetStatePath() => Path.Combine(_tempRoot, ".launcher", "state", "oobe-state.json");
|
||||
private string GetStatePath() => Path.Combine(_tempRoot, "Launcher", "state", "oobe-state.json");
|
||||
|
||||
private string GetLegacyMarkerPath() => Path.Combine(_tempRoot, ".launcher", "state", "first_run_completed");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using LanMountainDesktop.Launcher.Services;
|
||||
using System.IO.Compression;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
@@ -26,6 +27,96 @@ public sealed class PluginInstallerServiceTests : IDisposable
|
||||
Assert.Equal("plugin_elevation_required", result.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstallPackage_InstallsLaappWithPluginJson_InsideUserScope()
|
||||
{
|
||||
var packagePath = Path.Combine(_tempRoot, "sample.laapp");
|
||||
Directory.CreateDirectory(_tempRoot);
|
||||
CreatePluginPackage(packagePath, "plugin.json", "plugin.install.sample", "Sample Plugin");
|
||||
|
||||
var pluginsDirectory = CreateUserScopedPluginsDirectory();
|
||||
var service = new PluginInstallerService();
|
||||
|
||||
var result = service.InstallPackage(packagePath, pluginsDirectory);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("ok", result.Code);
|
||||
Assert.Equal("plugin.install.sample", result.ManifestId);
|
||||
Assert.Equal("Sample Plugin", result.ManifestName);
|
||||
Assert.NotNull(result.InstalledPackagePath);
|
||||
Assert.True(File.Exists(result.InstalledPackagePath));
|
||||
Assert.EndsWith(".laapp", result.InstalledPackagePath, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Empty(Directory.EnumerateFiles(pluginsDirectory, "*.incoming", SearchOption.AllDirectories));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstallPackage_ReplacesExistingPackageWithSamePluginId()
|
||||
{
|
||||
Directory.CreateDirectory(_tempRoot);
|
||||
var firstPackagePath = Path.Combine(_tempRoot, "sample-1.laapp");
|
||||
var secondPackagePath = Path.Combine(_tempRoot, "sample-2.laapp");
|
||||
CreatePluginPackage(firstPackagePath, "plugin.json", "plugin.replace.sample", "Sample Plugin v1");
|
||||
CreatePluginPackage(secondPackagePath, "plugin.json", "plugin.replace.sample", "Sample Plugin v2");
|
||||
|
||||
var pluginsDirectory = CreateUserScopedPluginsDirectory();
|
||||
var service = new PluginInstallerService();
|
||||
|
||||
var first = service.InstallPackage(firstPackagePath, pluginsDirectory);
|
||||
var second = service.InstallPackage(secondPackagePath, pluginsDirectory);
|
||||
|
||||
Assert.True(first.Success);
|
||||
Assert.True(second.Success);
|
||||
Assert.Single(Directory.EnumerateFiles(pluginsDirectory, "*.laapp", SearchOption.TopDirectoryOnly));
|
||||
Assert.True(File.Exists(second.InstalledPackagePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstallPackage_StillSupportsLegacyManifestJson()
|
||||
{
|
||||
var packagePath = Path.Combine(_tempRoot, "legacy.lmdp");
|
||||
Directory.CreateDirectory(_tempRoot);
|
||||
CreatePluginPackage(packagePath, "manifest.json", "plugin.legacy.sample", "Legacy Plugin");
|
||||
|
||||
var pluginsDirectory = CreateUserScopedPluginsDirectory();
|
||||
var service = new PluginInstallerService();
|
||||
|
||||
var result = service.InstallPackage(packagePath, pluginsDirectory);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("plugin.legacy.sample", result.ManifestId);
|
||||
Assert.True(File.Exists(result.InstalledPackagePath));
|
||||
}
|
||||
|
||||
private static void CreatePluginPackage(string packagePath, string manifestFileName, string pluginId, string pluginName)
|
||||
{
|
||||
using var archive = ZipFile.Open(packagePath, ZipArchiveMode.Create);
|
||||
var entry = archive.CreateEntry(manifestFileName);
|
||||
using var stream = entry.Open();
|
||||
using var writer = new StreamWriter(stream);
|
||||
writer.Write(
|
||||
$$"""
|
||||
{
|
||||
"id": "{{pluginId}}",
|
||||
"name": "{{pluginName}}",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
private static string CreateUserScopedPluginsDirectory()
|
||||
{
|
||||
var root = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LanMountainDesktop",
|
||||
"Tests",
|
||||
nameof(PluginInstallerServiceTests),
|
||||
Guid.NewGuid().ToString("N"),
|
||||
"Extensions",
|
||||
"Plugins");
|
||||
Directory.CreateDirectory(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
|
||||
127
LanMountainDesktop.Tests/PluginMarketIndexDocumentTests.cs
Normal file
127
LanMountainDesktop.Tests/PluginMarketIndexDocumentTests.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using LanMountainDesktop.Services.PluginMarket;
|
||||
using System.Net;
|
||||
using Xunit;
|
||||
|
||||
namespace LanMountainDesktop.Tests;
|
||||
|
||||
public sealed class PluginMarketIndexDocumentTests
|
||||
{
|
||||
[Fact]
|
||||
public void Load_WithNestedV2Entry_MapsDisplayFieldsAndWorkspacePath()
|
||||
{
|
||||
var document = AirAppMarketIndexDocument.Load(CreateNestedIndexJson(), "test-index.json");
|
||||
var plugin = Assert.Single(document.Plugins);
|
||||
var source = Assert.Single(plugin.PackageSources);
|
||||
|
||||
Assert.Equal("LanMountainDesktop.SamplePlugin", plugin.Id);
|
||||
Assert.Equal("LanMountain Sample Plugin", plugin.Name);
|
||||
Assert.Equal("SDK v5 sample plugin.", plugin.Description);
|
||||
Assert.Equal("LanMountainDesktop", plugin.Author);
|
||||
Assert.Equal("0.4.0", plugin.Version);
|
||||
Assert.Equal("5.0.0", plugin.ApiVersion);
|
||||
Assert.Equal("0.0.1", plugin.MinHostVersion);
|
||||
Assert.Equal("https://raw.githubusercontent.com/wwiinnddyy/LanAirApp/main/airappmarket/assets/sample-plugin.svg", plugin.IconUrl);
|
||||
Assert.Equal("https://raw.githubusercontent.com/wwiinnddyy/LanMountainDesktop.SamplePlugin/main/README.md", plugin.ReadmeUrl);
|
||||
Assert.Equal("workspace://LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.0.4.0.laapp", source.Url);
|
||||
Assert.Equal(PluginPackageSourceKind.WorkspaceLocal, source.SourceKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrichAsync_WhenRepositoryMetadataUnavailable_PreservesNestedDisplayFields()
|
||||
{
|
||||
var document = AirAppMarketIndexDocument.Load(
|
||||
CreateNestedIndexJson("LanMountainDesktop.MissingPlugin"),
|
||||
"test-index.json");
|
||||
using var httpClient = new HttpClient(new NotFoundHandler());
|
||||
using var resolver = new AirAppMarketMetadataResolverService(httpClient);
|
||||
|
||||
var enriched = await resolver.EnrichAsync(document);
|
||||
var plugin = Assert.Single(enriched.Plugins);
|
||||
|
||||
Assert.Equal("LanMountain Sample Plugin", plugin.Name);
|
||||
Assert.Equal("SDK v5 sample plugin.", plugin.Description);
|
||||
Assert.Equal("LanMountainDesktop", plugin.Author);
|
||||
Assert.Equal("0.4.0", plugin.Version);
|
||||
Assert.Equal("5.0.0", plugin.ApiVersion);
|
||||
Assert.Equal("0.0.1", plugin.MinHostVersion);
|
||||
Assert.Equal("v0.4.0", plugin.ReleaseTag);
|
||||
Assert.Equal("LanMountainDesktop.SamplePlugin.0.4.0.laapp", plugin.ReleaseAssetName);
|
||||
Assert.Equal("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", plugin.Sha256);
|
||||
Assert.Equal(1024, plugin.PackageSizeBytes);
|
||||
}
|
||||
|
||||
private static string CreateNestedIndexJson(string repositoryName = "LanMountainDesktop.SamplePlugin")
|
||||
{
|
||||
return $$"""
|
||||
{
|
||||
"schemaVersion": "2.0.0",
|
||||
"sourceId": "official",
|
||||
"sourceName": "LanAirApp",
|
||||
"generatedAt": "2026-04-29T00:00:00Z",
|
||||
"contracts": [],
|
||||
"plugins": [
|
||||
{
|
||||
"manifest": {
|
||||
"id": "LanMountainDesktop.SamplePlugin",
|
||||
"name": "LanMountain Sample Plugin",
|
||||
"description": "SDK v5 sample plugin.",
|
||||
"author": "LanMountainDesktop",
|
||||
"version": "0.4.0",
|
||||
"apiVersion": "5.0.0",
|
||||
"entranceAssembly": "LanMountainDesktop.SamplePlugin.dll",
|
||||
"sharedContracts": []
|
||||
},
|
||||
"compatibility": {
|
||||
"minHostVersion": "0.0.1",
|
||||
"apiVersion": "5.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"projectUrl": "https://github.com/wwiinnddyy/{{repositoryName}}",
|
||||
"readmeUrl": "https://raw.githubusercontent.com/wwiinnddyy/{{repositoryName}}/main/README.md",
|
||||
"homepageUrl": "https://github.com/wwiinnddyy/{{repositoryName}}",
|
||||
"repositoryUrl": "https://github.com/wwiinnddyy/{{repositoryName}}",
|
||||
"iconUrl": "https://raw.githubusercontent.com/wwiinnddyy/LanAirApp/main/airappmarket/assets/sample-plugin.svg",
|
||||
"tags": [ "official", "sdk" ],
|
||||
"releaseNotes": "Reference plugin for SDK v5 validation."
|
||||
},
|
||||
"publication": {
|
||||
"releaseTag": "v0.4.0",
|
||||
"releaseAssetName": "LanMountainDesktop.SamplePlugin.0.4.0.laapp",
|
||||
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"packageSizeBytes": 1024,
|
||||
"publishedAt": "2026-04-29T00:00:00Z",
|
||||
"updatedAt": "2026-04-29T00:00:00Z",
|
||||
"packageSources": [
|
||||
{
|
||||
"kind": "workspaceLocal",
|
||||
"path": "workspace://LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.0.4.0.laapp",
|
||||
"assetName": "LanMountainDesktop.SamplePlugin.0.4.0.laapp",
|
||||
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"sizeBytes": 1024,
|
||||
"releaseTag": "v0.4.0",
|
||||
"priority": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"desktopComponents": [ "LanMountainDesktop.SamplePlugin.StatusClock" ],
|
||||
"settingsSections": [ "status" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private sealed class NotFoundHandler : HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||
{
|
||||
RequestMessage = request,
|
||||
Content = new StringContent("{}")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using AvaloniaWebView;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.DesktopHost;
|
||||
using LanMountainDesktop.Models;
|
||||
@@ -78,7 +77,6 @@ public partial class App : Application
|
||||
private TransparentOverlayWindow? _transparentOverlayWindow;
|
||||
private FusedDesktopComponentLibraryWindow? _fusedComponentLibraryWindow;
|
||||
private bool _mainWindowClosed;
|
||||
private bool _uiUnhandledExceptionHooked;
|
||||
private DesktopShellHost? _desktopShellHost;
|
||||
private PublicIpcHostService? _publicIpcHostService;
|
||||
private LoadingStateManager? _loadingStateManager;
|
||||
@@ -194,8 +192,6 @@ public partial class App : Application
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigureWebViewUserDataFolder();
|
||||
AvaloniaWebViewBuilder.Initialize(default);
|
||||
ApplyThemeFromSettings();
|
||||
ApplyCurrentCultureFromSettings();
|
||||
EnsureSettingsWindowService();
|
||||
@@ -212,8 +208,7 @@ public partial class App : Application
|
||||
}
|
||||
|
||||
AppLogger.Info("App", "Framework initialization completed.");
|
||||
|
||||
RegisterUiUnhandledExceptionGuard();
|
||||
|
||||
LinuxDesktopEntryInstaller.EnsureInstalled();
|
||||
InitializePublicIpc();
|
||||
CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow);
|
||||
@@ -532,43 +527,8 @@ public partial class App : Application
|
||||
|
||||
private void DisableAvaloniaDataAnnotationValidation()
|
||||
{
|
||||
// Get an array of plugins to remove
|
||||
var dataValidationPluginsToRemove =
|
||||
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
|
||||
|
||||
// remove each entry found
|
||||
foreach (var plugin in dataValidationPluginsToRemove)
|
||||
{
|
||||
BindingPlugins.DataValidators.Remove(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureWebViewUserDataFolder()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const string userDataFolderEnvVar = "WEBVIEW2_USER_DATA_FOLDER";
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(userDataFolderEnvVar)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var userDataFolder = WebView2RuntimeProbe.ResolveUserDataFolder();
|
||||
Environment.SetEnvironmentVariable(
|
||||
userDataFolderEnvVar,
|
||||
userDataFolder,
|
||||
EnvironmentVariableTarget.Process);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Keep startup resilient if user profile folders are unavailable.
|
||||
AppLogger.Warn("WebView2", "Failed to configure WebView2 user data folder.", ex);
|
||||
}
|
||||
// Avalonia 12 中 BindingPlugins 已移除,数据验证插件不再需要手动禁用
|
||||
// 编译型绑定默认开启,数据注解验证行为已改变
|
||||
}
|
||||
|
||||
private void InitializePluginRuntime()
|
||||
@@ -1178,43 +1138,6 @@ public partial class App : Application
|
||||
_appearanceThemeService.ApplyThemeResources(Resources);
|
||||
}
|
||||
|
||||
private void RegisterUiUnhandledExceptionGuard()
|
||||
{
|
||||
if (_uiUnhandledExceptionHooked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.UnhandledException += OnUiThreadUnhandledException;
|
||||
_uiUnhandledExceptionHooked = true;
|
||||
}
|
||||
|
||||
private void OnUiThreadUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
if (!IsKnownWebViewStartupException(e.Exception))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
AppLogger.Warn(
|
||||
"WebView2",
|
||||
"Suppressed a known WebView startup exception from AvaloniaWebView.Navigate to keep the host process alive.",
|
||||
e.Exception);
|
||||
}
|
||||
|
||||
private static bool IsKnownWebViewStartupException(Exception exception)
|
||||
{
|
||||
if (exception is not NullReferenceException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stackTrace = exception.StackTrace ?? string.Empty;
|
||||
return stackTrace.Contains("AvaloniaWebView.WebView.Navigate", StringComparison.Ordinal) &&
|
||||
stackTrace.Contains("AvaloniaWebView.WebView.OnAttachedToVisualTree", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private void ReleaseSingleInstanceAfterExit(string source)
|
||||
{
|
||||
if (_singleInstanceReleased)
|
||||
@@ -1959,4 +1882,3 @@ public partial class App : Application
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
internal static class ComponentPreviewRuntimeQuiescer
|
||||
{
|
||||
private static readonly BindingFlags TimerMemberFlags =
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
|
||||
public static void Attach(Control control)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(control);
|
||||
|
||||
control.IsHitTestVisible = false;
|
||||
control.Focusable = false;
|
||||
control.AttachedToVisualTree += (_, _) =>
|
||||
Dispatcher.UIThread.Post(() => Quiesce(control), DispatcherPriority.Background);
|
||||
control.DetachedFromVisualTree += (_, _) => Quiesce(control);
|
||||
Quiesce(control);
|
||||
}
|
||||
|
||||
public static void Detach(Control control)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(control);
|
||||
|
||||
Quiesce(control);
|
||||
}
|
||||
|
||||
public static void Quiesce(Control control)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(control);
|
||||
|
||||
foreach (var candidate in EnumerateControls(control))
|
||||
{
|
||||
StopDispatcherTimers(candidate);
|
||||
candidate.IsHitTestVisible = false;
|
||||
candidate.Focusable = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Control> EnumerateControls(Control root)
|
||||
{
|
||||
yield return root;
|
||||
|
||||
foreach (var descendant in root.GetVisualDescendants().OfType<Control>())
|
||||
{
|
||||
yield return descendant;
|
||||
}
|
||||
}
|
||||
|
||||
private static void StopDispatcherTimers(object target)
|
||||
{
|
||||
var type = target.GetType();
|
||||
foreach (var field in type.GetFields(TimerMemberFlags))
|
||||
{
|
||||
if (typeof(DispatcherTimer).IsAssignableFrom(field.FieldType) &&
|
||||
field.GetValue(target) is DispatcherTimer timer)
|
||||
{
|
||||
timer.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var property in type.GetProperties(TimerMemberFlags))
|
||||
{
|
||||
if (!property.CanRead ||
|
||||
property.GetIndexParameters().Length != 0 ||
|
||||
!typeof(DispatcherTimer).IsAssignableFrom(property.PropertyType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (property.GetValue(target) is DispatcherTimer timer)
|
||||
{
|
||||
timer.Stop();
|
||||
}
|
||||
}
|
||||
catch (TargetInvocationException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public enum DesktopComponentRenderMode
|
||||
{
|
||||
Live = 0,
|
||||
LibraryPreview = 1
|
||||
}
|
||||
@@ -13,4 +13,5 @@ public sealed record DesktopComponentRuntimeContext(
|
||||
IAppearanceThemeService AppearanceTheme,
|
||||
ComponentChromeContext Chrome,
|
||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||
IComponentInstanceSettingsStore ComponentSettingsStore);
|
||||
IComponentInstanceSettingsStore ComponentSettingsStore,
|
||||
DesktopComponentRenderMode RenderMode = DesktopComponentRenderMode.Live);
|
||||
|
||||
@@ -108,7 +108,7 @@ public partial class SettingsOptionCard : UserControl
|
||||
"Info" => Symbol.Info,
|
||||
"ArrowSync" => Symbol.ArrowSync,
|
||||
"Alert" => Symbol.Alert,
|
||||
"Bell" => Symbol.Alert, // Bell也映射到Alert图标
|
||||
"Bell" => Symbol.AlertOn,
|
||||
_ => Symbol.Settings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ public partial class SettingsSectionCard : UserControl
|
||||
"PuzzlePiece" => Symbol.PuzzlePiece,
|
||||
"Info" => Symbol.Info,
|
||||
"ArrowSync" => Symbol.ArrowSync,
|
||||
"Bell" => Symbol.AlertOn,
|
||||
_ => Symbol.Settings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ internal sealed class ComponentLibraryCollapsePresenter
|
||||
{
|
||||
private static readonly TimeSpan TransitionDuration = TimeSpan.FromMilliseconds(150);
|
||||
private static readonly Easing TransitionEasing = new CubicEaseOut();
|
||||
private const double StableOpacityThreshold = 0.01;
|
||||
|
||||
private readonly Border _componentLibraryWindow;
|
||||
private readonly Border _collapsedChipHost;
|
||||
@@ -37,9 +36,7 @@ internal sealed class ComponentLibraryCollapsePresenter
|
||||
_collapsedChipIcon = collapsedChipIcon;
|
||||
|
||||
EnsureTransforms();
|
||||
_state = ComponentLibraryCollapseState.CreateExpanded(
|
||||
_componentLibraryWindow.Margin,
|
||||
_componentLibraryWindow.Opacity <= 0 ? 1 : _componentLibraryWindow.Opacity);
|
||||
_state = ComponentLibraryCollapseState.CreateExpanded(_componentLibraryWindow.Margin);
|
||||
ApplyExpandedSnapshot();
|
||||
_collapsedChipHost.IsVisible = false;
|
||||
_collapsedChipHost.IsHitTestVisible = false;
|
||||
@@ -50,19 +47,16 @@ internal sealed class ComponentLibraryCollapsePresenter
|
||||
|
||||
public ComponentLibraryCollapseVisualState VisualState => _state.VisualState;
|
||||
|
||||
public void SyncExpandedState(Thickness margin, double opacity)
|
||||
public void SyncExpandedState(Thickness margin)
|
||||
{
|
||||
var hasStableOpacity = IsStableExpandedOpacity(opacity);
|
||||
var nextExpandedOpacity = hasStableOpacity ? Math.Clamp(opacity, 0, 1) : _state.ExpandedOpacity;
|
||||
_state = _state with
|
||||
{
|
||||
ExpandedMargin = margin,
|
||||
ExpandedOpacity = nextExpandedOpacity
|
||||
ExpandedMargin = margin
|
||||
};
|
||||
|
||||
if (_state.VisualState is ComponentLibraryCollapseVisualState.Expanded or ComponentLibraryCollapseVisualState.Restoring)
|
||||
{
|
||||
ApplyExpandedSnapshot(applyOpacity: hasStableOpacity);
|
||||
ApplyExpandedSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +116,7 @@ internal sealed class ComponentLibraryCollapsePresenter
|
||||
return;
|
||||
}
|
||||
|
||||
_componentLibraryWindow.Opacity = _state.ExpandedOpacity;
|
||||
_componentLibraryWindow.Opacity = 1;
|
||||
_windowTranslate.Y = 0;
|
||||
},
|
||||
DispatcherPriority.Background);
|
||||
@@ -190,14 +184,10 @@ internal sealed class ComponentLibraryCollapsePresenter
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyExpandedSnapshot(bool applyOpacity = true)
|
||||
private void ApplyExpandedSnapshot()
|
||||
{
|
||||
_componentLibraryWindow.Margin = _state.ExpandedMargin;
|
||||
if (applyOpacity)
|
||||
{
|
||||
_componentLibraryWindow.Opacity = _state.ExpandedOpacity;
|
||||
}
|
||||
|
||||
_componentLibraryWindow.Opacity = 1;
|
||||
_componentLibraryWindow.IsVisible = true;
|
||||
_componentLibraryWindow.IsHitTestVisible = true;
|
||||
_windowTranslate.Y = 0;
|
||||
@@ -270,11 +260,4 @@ internal sealed class ComponentLibraryCollapsePresenter
|
||||
_componentLibraryWindow.Opacity = 0;
|
||||
_windowTranslate.Y = 28;
|
||||
}
|
||||
|
||||
private static bool IsStableExpandedOpacity(double opacity)
|
||||
{
|
||||
return !double.IsNaN(opacity) &&
|
||||
!double.IsInfinity(opacity) &&
|
||||
opacity > StableOpacityThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,13 @@ internal enum ComponentLibraryCollapseVisualState
|
||||
internal readonly record struct ComponentLibraryCollapseState(
|
||||
ComponentLibraryCollapseVisualState VisualState,
|
||||
Thickness ExpandedMargin,
|
||||
double ExpandedOpacity,
|
||||
bool IsChipVisible)
|
||||
{
|
||||
public static ComponentLibraryCollapseState CreateExpanded(Thickness expandedMargin, double expandedOpacity)
|
||||
public static ComponentLibraryCollapseState CreateExpanded(Thickness expandedMargin)
|
||||
{
|
||||
return new(
|
||||
ComponentLibraryCollapseVisualState.Expanded,
|
||||
expandedMargin,
|
||||
expandedOpacity,
|
||||
IsChipVisible: false);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,44 +41,42 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.12" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.12">
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Avalonia.Controls.WebView" />
|
||||
<PackageReference Include="Avalonia.Desktop" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||
<!--Condition below is needed to remove developer tools support from build output in Release configuration.-->
|
||||
<PackageReference Include="AvaloniaUI.DiagnosticsSupport">
|
||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||
<PackageReference Include="DotNetCampus.AvaloniaInkCanvas" Version="1.0.1" />
|
||||
<PackageReference Include="Downloader" Version="4.1.1" />
|
||||
<PackageReference Include="FluentAvaloniaUI" Version="2.5.0" />
|
||||
<PackageReference Include="FluentIcons.Avalonia" Version="2.0.320" />
|
||||
<PackageReference Include="FluentIcons.Avalonia.Fluent" Version="2.0.320" />
|
||||
<PackageReference Include="Material.Avalonia" Version="3.13.4" />
|
||||
<PackageReference Include="Material.Icons.Avalonia" Version="2.4.1" />
|
||||
<PackageReference Include="ClassIsland.Markdown.Avalonia" Version="11.0.3.4" />
|
||||
<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="MudTools.OfficeInterop" Version="2.0.8" />
|
||||
<PackageReference Include="MudTools.OfficeInterop.Word" Version="2.0.8" />
|
||||
<PackageReference Include="MudTools.OfficeInterop.Excel" Version="2.0.8" />
|
||||
<PackageReference Include="MudTools.OfficeInterop.PowerPoint" Version="2.0.8" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="DotNetCampus.AvaloniaInkCanvas" />
|
||||
<PackageReference Include="Downloader" />
|
||||
<PackageReference Include="FluentAvaloniaUI" />
|
||||
<PackageReference Include="FluentIcons.Avalonia" />
|
||||
<PackageReference Include="Material.Avalonia" />
|
||||
<PackageReference Include="Material.Icons.Avalonia" />
|
||||
<PackageReference Include="ClassIsland.Markdown.Avalonia" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||
<PackageReference Include="MudTools.OfficeInterop" />
|
||||
<PackageReference Include="MudTools.OfficeInterop.Word" />
|
||||
<PackageReference Include="MudTools.OfficeInterop.Excel" />
|
||||
<PackageReference Include="MudTools.OfficeInterop.PowerPoint" />
|
||||
|
||||
<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="PortAudioSharp2" />
|
||||
<PackageReference Include="MaterialColorUtilities" />
|
||||
<PackageReference Include="PostHog" />
|
||||
<PackageReference Include="Sentry" />
|
||||
<PackageReference Include="System.Runtime.WindowsRuntime" />
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
|
||||
<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" />
|
||||
<PackageReference Include="Tmds.DBus.Protocol" Version="0.92.0" />
|
||||
<PackageReference Include="log4net" Version="3.3.0" />
|
||||
<PackageReference Include="YamlDotNet" />
|
||||
<PackageReference Include="Tmds.DBus.Protocol" />
|
||||
<PackageReference Include="log4net" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Launcher 构建目标已移除 - Launcher 现在是独立应用,由 CI/CD 单独构建 -->
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.WebView.Desktop;
|
||||
using LanMountainDesktop.DesktopHost;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Plugins;
|
||||
@@ -111,7 +110,6 @@ public sealed class Program
|
||||
{
|
||||
var builder = AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.UseDesktopWebView()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
|
||||
|
||||
@@ -92,7 +92,8 @@ internal sealed class ComponentLibraryService : IComponentLibraryService
|
||||
context.RecommendationInfoService,
|
||||
context.CalculatorDataService,
|
||||
context.SettingsFacade,
|
||||
context.PlacementId);
|
||||
context.PlacementId,
|
||||
context.RenderMode);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public sealed class ComponentPreviewImageService : IComponentPreviewImageService
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly Dictionary<ComponentPreviewKey, ComponentPreviewImageEntry> _entries = new(ComponentPreviewKeyComparer.Instance);
|
||||
private readonly Dictionary<ComponentPreviewKey, Task<ComponentPreviewImageEntry>> _inFlightRequests = new(ComponentPreviewKeyComparer.Instance);
|
||||
private Task _queueTail = Task.CompletedTask;
|
||||
|
||||
public ComponentPreviewImageEntry GetOrCreateEntry(ComponentPreviewKey key, string? visualSignature = null)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_entries.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var created = new ComponentPreviewImageEntry(key, visualSignature);
|
||||
_entries[key] = created;
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetEntry(ComponentPreviewKey key, out ComponentPreviewImageEntry? entry)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_entries.TryGetValue(key, out var existing))
|
||||
{
|
||||
entry = existing;
|
||||
return true;
|
||||
}
|
||||
|
||||
entry = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ComponentPreviewImageEntry> GetEntriesSnapshot()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _entries.Values.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<ComponentPreviewImageEntry> QueueGenerationAsync(
|
||||
ComponentPreviewKey key,
|
||||
string visualSignature,
|
||||
Func<CancellationToken, Task<IImage?>> generationWork,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(generationWork);
|
||||
|
||||
var normalizedSignature = NormalizeRequired(visualSignature, nameof(visualSignature));
|
||||
lock (_gate)
|
||||
{
|
||||
var entry = GetOrCreateEntryCore(key);
|
||||
|
||||
if (entry.State == ComponentPreviewImageState.Ready &&
|
||||
entry.Bitmap is not null &&
|
||||
StringComparer.Ordinal.Equals(entry.VisualSignature, normalizedSignature))
|
||||
{
|
||||
return Task.FromResult(entry);
|
||||
}
|
||||
|
||||
if (_inFlightRequests.TryGetValue(key, out var inFlight))
|
||||
{
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
var expectedRevision = entry.BeginGeneration(normalizedSignature);
|
||||
var previousTask = _queueTail;
|
||||
var queuedTask = RunGenerationAsync(
|
||||
previousTask,
|
||||
key,
|
||||
entry,
|
||||
expectedRevision,
|
||||
normalizedSignature,
|
||||
generationWork,
|
||||
cancellationToken);
|
||||
|
||||
_inFlightRequests[key] = queuedTask;
|
||||
_queueTail = queuedTask.ContinueWith(
|
||||
static _ => { },
|
||||
CancellationToken.None,
|
||||
TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Default);
|
||||
return queuedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public ComponentPreviewImageEntry Store(ComponentPreviewKey key, IImage bitmap, string visualSignature)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bitmap);
|
||||
|
||||
var normalizedSignature = NormalizeRequired(visualSignature, nameof(visualSignature));
|
||||
lock (_gate)
|
||||
{
|
||||
var entry = GetOrCreateEntryCore(key);
|
||||
entry.StoreBitmap(bitmap, normalizedSignature);
|
||||
_inFlightRequests.Remove(key);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
public ComponentPreviewImageEntry StoreFailure(ComponentPreviewKey key, string visualSignature, string? errorMessage = null)
|
||||
{
|
||||
var normalizedSignature = NormalizeRequired(visualSignature, nameof(visualSignature));
|
||||
lock (_gate)
|
||||
{
|
||||
var entry = GetOrCreateEntryCore(key);
|
||||
entry.StoreFailure(normalizedSignature, errorMessage);
|
||||
_inFlightRequests.Remove(key);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Invalidate(ComponentPreviewKey key, string? visualSignature = null)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_entries.TryGetValue(key, out var entry))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
entry.Invalidate(visualSignature);
|
||||
_inFlightRequests.Remove(key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public int RemovePlacementPreviews(string placementId)
|
||||
{
|
||||
var normalizedPlacementId = NormalizeRequired(placementId, nameof(placementId));
|
||||
lock (_gate)
|
||||
{
|
||||
var entriesToRemove = _entries
|
||||
.Where(static pair => pair.Key.Kind == ComponentPreviewKeyKind.PlacementInstance)
|
||||
.Where(pair => StringComparer.OrdinalIgnoreCase.Equals(pair.Key.PlacementId, normalizedPlacementId))
|
||||
.ToArray();
|
||||
|
||||
foreach (var pair in entriesToRemove)
|
||||
{
|
||||
pair.Value.DisposeBitmap();
|
||||
_entries.Remove(pair.Key);
|
||||
_inFlightRequests.Remove(pair.Key);
|
||||
}
|
||||
|
||||
return entriesToRemove.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public int InvalidateVisualSignature(string visualSignature)
|
||||
{
|
||||
var normalizedSignature = NormalizeRequired(visualSignature, nameof(visualSignature));
|
||||
lock (_gate)
|
||||
{
|
||||
var entriesToInvalidate = _entries.Values
|
||||
.Where(entry => StringComparer.Ordinal.Equals(entry.VisualSignature, normalizedSignature))
|
||||
.ToArray();
|
||||
|
||||
foreach (var entry in entriesToInvalidate)
|
||||
{
|
||||
entry.Invalidate(normalizedSignature);
|
||||
_inFlightRequests.Remove(entry.Key);
|
||||
}
|
||||
|
||||
return entriesToInvalidate.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ComponentPreviewImageEntry> RunGenerationAsync(
|
||||
Task previousTask,
|
||||
ComponentPreviewKey key,
|
||||
ComponentPreviewImageEntry entry,
|
||||
long expectedRevision,
|
||||
string visualSignature,
|
||||
Func<CancellationToken, Task<IImage?>> generationWork,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
await previousTask.ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep serial queue processing even if previous work faulted.
|
||||
}
|
||||
|
||||
IImage? bitmap;
|
||||
try
|
||||
{
|
||||
bitmap = await generationWork(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
entry.TryApplyFailure(expectedRevision, visualSignature, ex.Message);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (bitmap is null)
|
||||
{
|
||||
entry.TryApplyFailure(expectedRevision, visualSignature, "Preview generation returned no bitmap.");
|
||||
}
|
||||
else
|
||||
{
|
||||
entry.TryApplyGeneratedBitmap(expectedRevision, bitmap, visualSignature);
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_inFlightRequests.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ComponentPreviewImageEntry GetOrCreateEntryCore(ComponentPreviewKey key)
|
||||
{
|
||||
if (_entries.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var created = new ComponentPreviewImageEntry(key);
|
||||
_entries[key] = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
private static string NormalizeRequired(string? value, string paramName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("Value cannot be null or whitespace.", paramName);
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Media;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public enum ComponentPreviewKeyKind
|
||||
{
|
||||
ComponentType = 0,
|
||||
PlacementInstance = 1
|
||||
}
|
||||
|
||||
public readonly record struct ComponentPreviewKey
|
||||
{
|
||||
private ComponentPreviewKey(
|
||||
ComponentPreviewKeyKind kind,
|
||||
string componentTypeId,
|
||||
string? placementId,
|
||||
int widthCells,
|
||||
int heightCells)
|
||||
{
|
||||
Kind = kind;
|
||||
ComponentTypeId = NormalizeRequired(componentTypeId, nameof(componentTypeId));
|
||||
PlacementId = kind == ComponentPreviewKeyKind.PlacementInstance
|
||||
? NormalizeRequired(placementId, nameof(placementId))
|
||||
: null;
|
||||
WidthCells = NormalizeSpan(widthCells, nameof(widthCells));
|
||||
HeightCells = NormalizeSpan(heightCells, nameof(heightCells));
|
||||
}
|
||||
|
||||
public ComponentPreviewKeyKind Kind { get; }
|
||||
|
||||
public string ComponentTypeId { get; }
|
||||
|
||||
public string? PlacementId { get; }
|
||||
|
||||
public int WidthCells { get; }
|
||||
|
||||
public int HeightCells { get; }
|
||||
|
||||
public static ComponentPreviewKey ForComponentType(string componentTypeId, int widthCells, int heightCells)
|
||||
{
|
||||
return new ComponentPreviewKey(ComponentPreviewKeyKind.ComponentType, componentTypeId, null, widthCells, heightCells);
|
||||
}
|
||||
|
||||
public static ComponentPreviewKey ForPlacementInstance(string componentTypeId, string placementId, int widthCells, int heightCells)
|
||||
{
|
||||
return new ComponentPreviewKey(
|
||||
ComponentPreviewKeyKind.PlacementInstance,
|
||||
componentTypeId,
|
||||
placementId,
|
||||
widthCells,
|
||||
heightCells);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Kind == ComponentPreviewKeyKind.ComponentType
|
||||
? $"Type:{ComponentTypeId}[{WidthCells}x{HeightCells}]"
|
||||
: $"Placement:{ComponentTypeId}@{PlacementId}[{WidthCells}x{HeightCells}]";
|
||||
}
|
||||
|
||||
private static string NormalizeRequired(string? value, string paramName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("Value cannot be null or whitespace.", paramName);
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
|
||||
private static int NormalizeSpan(int value, string paramName)
|
||||
{
|
||||
if (value <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName, value, "Span must be greater than zero.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ComponentPreviewImageState
|
||||
{
|
||||
Pending = 0,
|
||||
Ready = 1,
|
||||
Failed = 2
|
||||
}
|
||||
|
||||
public sealed class ComponentPreviewImageEntry : ObservableObject
|
||||
{
|
||||
private IImage? _bitmap;
|
||||
private ComponentPreviewImageState _state = ComponentPreviewImageState.Pending;
|
||||
private string _visualSignature = string.Empty;
|
||||
private string? _errorMessage;
|
||||
private long _revision;
|
||||
private DateTimeOffset _lastUpdatedUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
public ComponentPreviewImageEntry(ComponentPreviewKey key, string? visualSignature = null)
|
||||
{
|
||||
Key = key;
|
||||
VisualSignature = NormalizeSignature(visualSignature);
|
||||
}
|
||||
|
||||
public ComponentPreviewKey Key { get; }
|
||||
|
||||
public IImage? Bitmap
|
||||
{
|
||||
get => _bitmap;
|
||||
private set => SetProperty(ref _bitmap, value);
|
||||
}
|
||||
|
||||
public ComponentPreviewImageState State
|
||||
{
|
||||
get => _state;
|
||||
private set => SetProperty(ref _state, value);
|
||||
}
|
||||
|
||||
public string VisualSignature
|
||||
{
|
||||
get => _visualSignature;
|
||||
private set => SetProperty(ref _visualSignature, value);
|
||||
}
|
||||
|
||||
public string? ErrorMessage
|
||||
{
|
||||
get => _errorMessage;
|
||||
private set => SetProperty(ref _errorMessage, value);
|
||||
}
|
||||
|
||||
public long Revision
|
||||
{
|
||||
get => _revision;
|
||||
private set => SetProperty(ref _revision, value);
|
||||
}
|
||||
|
||||
public DateTimeOffset LastUpdatedUtc
|
||||
{
|
||||
get => _lastUpdatedUtc;
|
||||
private set => SetProperty(ref _lastUpdatedUtc, value);
|
||||
}
|
||||
|
||||
internal long BeginGeneration(string visualSignature)
|
||||
{
|
||||
var normalizedVisualSignature = NormalizeSignature(visualSignature);
|
||||
var nextRevision = Revision + 1;
|
||||
Revision = nextRevision;
|
||||
VisualSignature = normalizedVisualSignature;
|
||||
State = ComponentPreviewImageState.Pending;
|
||||
ReplaceBitmap(null);
|
||||
ErrorMessage = null;
|
||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
||||
return nextRevision;
|
||||
}
|
||||
|
||||
internal bool TryApplyGeneratedBitmap(long expectedRevision, IImage bitmap, string visualSignature)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bitmap);
|
||||
|
||||
if (Revision != expectedRevision)
|
||||
{
|
||||
DisposeIfNeeded(bitmap);
|
||||
return false;
|
||||
}
|
||||
|
||||
VisualSignature = NormalizeSignature(visualSignature);
|
||||
State = ComponentPreviewImageState.Ready;
|
||||
ReplaceBitmap(bitmap);
|
||||
ErrorMessage = null;
|
||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal bool TryApplyFailure(long expectedRevision, string visualSignature, string? errorMessage)
|
||||
{
|
||||
if (Revision != expectedRevision)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
VisualSignature = NormalizeSignature(visualSignature);
|
||||
State = ComponentPreviewImageState.Failed;
|
||||
ReplaceBitmap(null);
|
||||
ErrorMessage = string.IsNullOrWhiteSpace(errorMessage) ? "Unknown preview generation failure." : errorMessage.Trim();
|
||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void StoreBitmap(IImage bitmap, string visualSignature)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bitmap);
|
||||
|
||||
Revision += 1;
|
||||
VisualSignature = NormalizeSignature(visualSignature);
|
||||
State = ComponentPreviewImageState.Ready;
|
||||
ReplaceBitmap(bitmap);
|
||||
ErrorMessage = null;
|
||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
internal void StoreFailure(string visualSignature, string? errorMessage)
|
||||
{
|
||||
Revision += 1;
|
||||
VisualSignature = NormalizeSignature(visualSignature);
|
||||
State = ComponentPreviewImageState.Failed;
|
||||
ReplaceBitmap(null);
|
||||
ErrorMessage = string.IsNullOrWhiteSpace(errorMessage) ? "Unknown preview generation failure." : errorMessage.Trim();
|
||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
internal void Invalidate(string? visualSignature = null)
|
||||
{
|
||||
Revision += 1;
|
||||
if (visualSignature is not null)
|
||||
{
|
||||
VisualSignature = NormalizeSignature(visualSignature);
|
||||
}
|
||||
|
||||
State = ComponentPreviewImageState.Pending;
|
||||
ReplaceBitmap(null);
|
||||
ErrorMessage = null;
|
||||
LastUpdatedUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
internal void DisposeBitmap()
|
||||
{
|
||||
ReplaceBitmap(null);
|
||||
}
|
||||
|
||||
private void ReplaceBitmap(IImage? bitmap)
|
||||
{
|
||||
var previous = _bitmap;
|
||||
if (ReferenceEquals(previous, bitmap))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap = bitmap;
|
||||
DisposeIfNeeded(previous);
|
||||
}
|
||||
|
||||
private static void DisposeIfNeeded(IImage? bitmap)
|
||||
{
|
||||
if (bitmap is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeSignature(string? visualSignature)
|
||||
{
|
||||
return visualSignature?.Trim() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ComponentPreviewKeyComparer : IEqualityComparer<ComponentPreviewKey>
|
||||
{
|
||||
public static ComponentPreviewKeyComparer Instance { get; } = new();
|
||||
|
||||
public bool Equals(ComponentPreviewKey x, ComponentPreviewKey y)
|
||||
{
|
||||
return x.Kind == y.Kind &&
|
||||
StringComparer.OrdinalIgnoreCase.Equals(x.ComponentTypeId, y.ComponentTypeId) &&
|
||||
StringComparer.OrdinalIgnoreCase.Equals(x.PlacementId, y.PlacementId) &&
|
||||
x.WidthCells == y.WidthCells &&
|
||||
x.HeightCells == y.HeightCells;
|
||||
}
|
||||
|
||||
public int GetHashCode(ComponentPreviewKey obj)
|
||||
{
|
||||
var hash = new HashCode();
|
||||
hash.Add(obj.Kind);
|
||||
hash.Add(obj.ComponentTypeId, StringComparer.OrdinalIgnoreCase);
|
||||
hash.Add(obj.PlacementId, StringComparer.OrdinalIgnoreCase);
|
||||
hash.Add(obj.WidthCells);
|
||||
hash.Add(obj.HeightCells);
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,8 @@ public sealed record ComponentLibraryCreateContext(
|
||||
IRecommendationInfoService RecommendationInfoService,
|
||||
ICalculatorDataService CalculatorDataService,
|
||||
ISettingsFacadeService SettingsFacade,
|
||||
string? PlacementId = null);
|
||||
string? PlacementId = null,
|
||||
DesktopComponentRenderMode RenderMode = DesktopComponentRenderMode.Live);
|
||||
|
||||
public interface IComponentLibraryService
|
||||
{
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public interface IComponentPreviewImageService
|
||||
{
|
||||
ComponentPreviewImageEntry GetOrCreateEntry(ComponentPreviewKey key, string? visualSignature = null);
|
||||
|
||||
bool TryGetEntry(ComponentPreviewKey key, out ComponentPreviewImageEntry? entry);
|
||||
|
||||
IReadOnlyCollection<ComponentPreviewImageEntry> GetEntriesSnapshot();
|
||||
|
||||
Task<ComponentPreviewImageEntry> QueueGenerationAsync(
|
||||
ComponentPreviewKey key,
|
||||
string visualSignature,
|
||||
Func<CancellationToken, Task<IImage?>> generationWork,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
ComponentPreviewImageEntry Store(ComponentPreviewKey key, IImage bitmap, string visualSignature);
|
||||
|
||||
ComponentPreviewImageEntry StoreFailure(ComponentPreviewKey key, string visualSignature, string? errorMessage = null);
|
||||
|
||||
bool Invalidate(ComponentPreviewKey key, string? visualSignature = null);
|
||||
|
||||
int RemovePlacementPreviews(string placementId);
|
||||
|
||||
int InvalidateVisualSignature(string visualSignature);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -111,7 +112,7 @@ internal sealed class LauncherClient
|
||||
WorkingDirectory = Path.GetDirectoryName(launcherPath) ?? AppContext.BaseDirectory,
|
||||
Arguments = string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"--source {QuoteArgument(Path.GetFullPath(packagePath))} --plugins-dir {QuoteArgument(Path.GetFullPath(pluginsDirectory))} --result {QuoteArgument(Path.GetFullPath(resultPath))} --launch-source plugin-install")
|
||||
$"plugin install --source {QuoteArgument(Path.GetFullPath(packagePath))} --plugins-dir {QuoteArgument(Path.GetFullPath(pluginsDirectory))} --result {QuoteArgument(Path.GetFullPath(resultPath))} --launch-source plugin-install")
|
||||
};
|
||||
|
||||
return Process.Start(startInfo);
|
||||
@@ -130,7 +131,17 @@ internal sealed class LauncherClient
|
||||
|
||||
private static string ResolveLauncherPath()
|
||||
{
|
||||
return Path.Combine(AppContext.BaseDirectory, "Launcher", LauncherExecutableName);
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(baseDirectory, "Launcher", LauncherExecutableName),
|
||||
Path.Combine(baseDirectory, LauncherExecutableName),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "LanMountainDesktop.Launcher", LauncherExecutableName)),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Debug", "net10.0", LauncherExecutableName)),
|
||||
Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", "LanMountainDesktop.Launcher", "bin", "Release", "net10.0", LauncherExecutableName))
|
||||
};
|
||||
|
||||
return candidates.FirstOrDefault(File.Exists) ?? candidates[0];
|
||||
}
|
||||
|
||||
private static string QuoteArgument(string value)
|
||||
|
||||
@@ -88,8 +88,6 @@ public sealed class MonetColorService
|
||||
PixelFormat.Bgra8888,
|
||||
AlphaFormat.Premul);
|
||||
using var framebuffer = writeable.Lock();
|
||||
scaledBitmap.CopyPixels(framebuffer, AlphaFormat.Premul);
|
||||
|
||||
var byteCount = framebuffer.RowBytes * framebuffer.Size.Height;
|
||||
if (byteCount <= 0 || framebuffer.Address == IntPtr.Zero)
|
||||
{
|
||||
@@ -97,6 +95,11 @@ public sealed class MonetColorService
|
||||
}
|
||||
|
||||
var pixelBuffer = new byte[byteCount];
|
||||
scaledBitmap.CopyPixels(
|
||||
new PixelRect(scaledBitmap.PixelSize),
|
||||
framebuffer.Address,
|
||||
byteCount,
|
||||
framebuffer.RowBytes);
|
||||
Marshal.Copy(framebuffer.Address, pixelBuffer, 0, byteCount);
|
||||
|
||||
var argbPixels = new List<uint>(framebuffer.Size.Width * framebuffer.Size.Height);
|
||||
|
||||
@@ -65,7 +65,7 @@ public interface INotificationService
|
||||
{
|
||||
void Show(NotificationContent content);
|
||||
|
||||
Task<ContentDialogResult> ShowDialogAsync(NotificationContent content);
|
||||
Task<FAContentDialogResult> ShowDialogAsync(NotificationContent content);
|
||||
|
||||
void ShowInfo(string title, string? message = null,
|
||||
NotificationPosition position = NotificationPosition.TopRight);
|
||||
@@ -79,17 +79,17 @@ public interface INotificationService
|
||||
void ShowError(string title, string? message = null,
|
||||
NotificationPosition position = NotificationPosition.TopRight);
|
||||
|
||||
Task<ContentDialogResult> ShowDialogInfoAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "确定", string? closeButtonText = "取消");
|
||||
Task<FAContentDialogResult> ShowDialogInfoAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "OK", string? closeButtonText = "Cancel");
|
||||
|
||||
Task<ContentDialogResult> ShowDialogSuccessAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "确定", string? closeButtonText = "取消");
|
||||
Task<FAContentDialogResult> ShowDialogSuccessAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "OK", string? closeButtonText = "Cancel");
|
||||
|
||||
Task<ContentDialogResult> ShowDialogWarningAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "确定", string? closeButtonText = "取消");
|
||||
Task<FAContentDialogResult> ShowDialogWarningAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "OK", string? closeButtonText = "Cancel");
|
||||
|
||||
Task<ContentDialogResult> ShowDialogErrorAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "确定", string? closeButtonText = "取消");
|
||||
Task<FAContentDialogResult> ShowDialogErrorAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "OK", string? closeButtonText = "Cancel");
|
||||
}
|
||||
|
||||
internal sealed class NotificationService : INotificationService
|
||||
@@ -105,20 +105,17 @@ internal sealed class NotificationService : INotificationService
|
||||
|
||||
public void Show(NotificationContent content)
|
||||
{
|
||||
// 检查通知开关是否启用
|
||||
if (!IsNotificationEnabled())
|
||||
{
|
||||
return; // 通知已禁用,不显示
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's a dialog notification (center position), show as dialog window
|
||||
if (content.IsDialogNotification)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => ShowDialogWindow(content), DispatcherPriority.Normal);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, show as toast notification
|
||||
Dispatcher.UIThread.Post(() => ShowCore(content), DispatcherPriority.Normal);
|
||||
}
|
||||
|
||||
@@ -153,37 +150,35 @@ internal sealed class NotificationService : INotificationService
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<ContentDialogResult> ShowDialogAsync(NotificationContent content)
|
||||
public async Task<FAContentDialogResult> ShowDialogAsync(NotificationContent content)
|
||||
{
|
||||
// 检查通知开关是否启用
|
||||
if (!IsNotificationEnabled())
|
||||
{
|
||||
return ContentDialogResult.None; // 通知已禁用,不显示
|
||||
return FAContentDialogResult.None;
|
||||
}
|
||||
|
||||
return await Dispatcher.UIThread.InvokeAsync(() => ShowDialogCoreAsync(content));
|
||||
}
|
||||
|
||||
private async Task<ContentDialogResult> ShowDialogCoreAsync(NotificationContent content)
|
||||
private async Task<FAContentDialogResult> ShowDialogCoreAsync(NotificationContent content)
|
||||
{
|
||||
// Get the main window as the dialog host
|
||||
var mainWindow = GetMainWindow();
|
||||
if (mainWindow is null)
|
||||
{
|
||||
AppLogger.Warn("Notification", "Cannot show dialog notification: main window not found");
|
||||
return ContentDialogResult.None;
|
||||
return FAContentDialogResult.None;
|
||||
}
|
||||
|
||||
var dialog = new ContentDialog
|
||||
var dialog = new FAContentDialog
|
||||
{
|
||||
Title = content.Title,
|
||||
Content = content.Message ?? string.Empty,
|
||||
PrimaryButtonText = content.PrimaryButtonText,
|
||||
SecondaryButtonText = content.SecondaryButtonText,
|
||||
CloseButtonText = content.CloseButtonText,
|
||||
DefaultButton = !string.IsNullOrEmpty(content.PrimaryButtonText) ? ContentDialogButton.Primary :
|
||||
!string.IsNullOrEmpty(content.SecondaryButtonText) ? ContentDialogButton.Secondary :
|
||||
ContentDialogButton.Close
|
||||
DefaultButton = !string.IsNullOrEmpty(content.PrimaryButtonText) ? FAContentDialogButton.Primary :
|
||||
!string.IsNullOrEmpty(content.SecondaryButtonText) ? FAContentDialogButton.Secondary :
|
||||
FAContentDialogButton.Close
|
||||
};
|
||||
|
||||
var result = await dialog.ShowAsync(mainWindow);
|
||||
@@ -191,10 +186,10 @@ internal sealed class NotificationService : INotificationService
|
||||
// Execute callbacks based on result
|
||||
switch (result)
|
||||
{
|
||||
case ContentDialogResult.Primary:
|
||||
case FAContentDialogResult.Primary:
|
||||
content.OnPrimaryButtonClick?.Invoke();
|
||||
break;
|
||||
case ContentDialogResult.Secondary:
|
||||
case FAContentDialogResult.Secondary:
|
||||
content.OnSecondaryButtonClick?.Invoke();
|
||||
break;
|
||||
}
|
||||
@@ -206,14 +201,13 @@ internal sealed class NotificationService : INotificationService
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从全局设置服务中读取通知开关状态
|
||||
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
var snapshot = settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(PluginSdk.SettingsScope.App);
|
||||
return snapshot.NotificationEnabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果读取失败,默认启用通知
|
||||
// 濠电姷顣介埀顒€鍟块埀顒€缍婇幃妯诲緞鐏炴儳鐝伴柣鐘叉处瑜板啰寰婇崹顕呯唵闁诡垱澹嗙花鍧楁偡濞嗘瑧鐣甸柡浣哥Т閻f繈宕熼鐐殿偧闂佽崵濮抽梽宥夊磹閺囥垹绠氶幖娣妽閸嬨劑鏌曟繛鐐澒闁稿鎸搁~婵囨綇閳轰礁缁?
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -286,8 +280,8 @@ internal sealed class NotificationService : INotificationService
|
||||
Show(new NotificationContent(title, message, Severity: NotificationSeverity.Error, Position: position));
|
||||
}
|
||||
|
||||
public Task<ContentDialogResult> ShowDialogInfoAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "确定", string? closeButtonText = "取消")
|
||||
public Task<FAContentDialogResult> ShowDialogInfoAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "OK", string? closeButtonText = "Cancel")
|
||||
{
|
||||
return ShowDialogAsync(new NotificationContent(
|
||||
title,
|
||||
@@ -298,8 +292,8 @@ internal sealed class NotificationService : INotificationService
|
||||
CloseButtonText: closeButtonText));
|
||||
}
|
||||
|
||||
public Task<ContentDialogResult> ShowDialogSuccessAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "确定", string? closeButtonText = "取消")
|
||||
public Task<FAContentDialogResult> ShowDialogSuccessAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "OK", string? closeButtonText = "Cancel")
|
||||
{
|
||||
return ShowDialogAsync(new NotificationContent(
|
||||
title,
|
||||
@@ -310,8 +304,8 @@ internal sealed class NotificationService : INotificationService
|
||||
CloseButtonText: closeButtonText));
|
||||
}
|
||||
|
||||
public Task<ContentDialogResult> ShowDialogWarningAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "确定", string? closeButtonText = "取消")
|
||||
public Task<FAContentDialogResult> ShowDialogWarningAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "OK", string? closeButtonText = "Cancel")
|
||||
{
|
||||
return ShowDialogAsync(new NotificationContent(
|
||||
title,
|
||||
@@ -322,8 +316,8 @@ internal sealed class NotificationService : INotificationService
|
||||
CloseButtonText: closeButtonText));
|
||||
}
|
||||
|
||||
public Task<ContentDialogResult> ShowDialogErrorAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "确定", string? closeButtonText = "取消")
|
||||
public Task<FAContentDialogResult> ShowDialogErrorAsync(string title, string? message = null,
|
||||
string? primaryButtonText = "OK", string? closeButtonText = "Cancel")
|
||||
{
|
||||
return ShowDialogAsync(new NotificationContent(
|
||||
title,
|
||||
@@ -357,7 +351,7 @@ internal sealed class NotificationWindowManager
|
||||
var position = viewModel.Position;
|
||||
var windows = _windowsByPosition[position];
|
||||
|
||||
// 从设置中读取最大通知数量
|
||||
// 濠电偛顕慨鏉戭潩閿曞偆鏁婇柡鍥╁Х绾剧偓銇勯弮鈧Σ鎺楀储椤掑嫭鍋i柛銉憾閸ゆ瑧鎲搁弶鎸庡枠鐎殿喚鏁婚崺鈧い鎺嶇缁剁偟鎲搁弮鍫濈劦妞ゆ帊鐒﹂惃鎴︽煟閹垮嫮绡€鐎殿噮鍋呯€靛ジ寮堕幋鐑嗕画
|
||||
var maxNotifications = GetMaxNotificationsPerPosition();
|
||||
|
||||
if (windows.Count >= maxNotifications)
|
||||
@@ -395,14 +389,13 @@ internal sealed class NotificationWindowManager
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从全局设置服务中读取最大通知数量
|
||||
// 濠电偛顕慨瀛橆殽閹间礁鐭楅煫鍥ㄦ磻濞岊亪鏌嶈閸撴盯骞忕€n喖绀堢憸蹇涘几閸岀偞鐓涢柛顐g箘瀛濇繝娈垮枤閸犳劗绮欐径鎰垫晣闁宠棄妫楀▓娲⒑閸涘﹦鎳勯柣妤侇殔閵嗘帡鎳滈棃娑氱獮閻熸粍妫冮崺鈧い鎺嶇劍閻ㄦ垿鏌i幙鍕瘈鐎殿噮鍋呯€靛ジ寮堕幋鐑嗕画
|
||||
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
var snapshot = settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(PluginSdk.SettingsScope.App);
|
||||
return snapshot.NotificationMaxPerPosition > 0 ? snapshot.NotificationMaxPerPosition : 5;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果读取失败,返回默认值
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1236,7 +1236,31 @@ internal sealed class PluginCatalogSettingsService : IPluginCatalogSettingsServi
|
||||
repository,
|
||||
publication,
|
||||
sources,
|
||||
[]);
|
||||
BuildCapabilities(entry));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PluginCapabilityInfo> BuildCapabilities(AirAppMarketPluginEntry entry)
|
||||
{
|
||||
if (entry.Capabilities is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var capabilities = new List<PluginCapabilityInfo>();
|
||||
capabilities.AddRange(entry.Capabilities.SharedContracts.Select(contract =>
|
||||
new PluginCapabilityInfo(contract.Id, contract.Version, contract.AssemblyName)));
|
||||
capabilities.AddRange(entry.Capabilities.DesktopComponents.Select(id =>
|
||||
new PluginCapabilityInfo(id, null, null)));
|
||||
capabilities.AddRange(entry.Capabilities.SettingsSections.Select(id =>
|
||||
new PluginCapabilityInfo(id, null, null)));
|
||||
capabilities.AddRange(entry.Capabilities.Exports.Select(id =>
|
||||
new PluginCapabilityInfo(id, null, null)));
|
||||
capabilities.AddRange(entry.Capabilities.MessageTypes.Select(id =>
|
||||
new PluginCapabilityInfo(id, null, null)));
|
||||
|
||||
return capabilities
|
||||
.DistinctBy(capability => $"{capability.Id}@{capability.Version}@{capability.AssemblyName}")
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PluginPackageSourceInfo> BuildPackageSources(AirAppMarketPluginEntry entry)
|
||||
|
||||
@@ -19,7 +19,7 @@ public static class WebView2RuntimeProbe
|
||||
|
||||
public static WebView2RuntimeAvailability GetAvailability()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return new WebView2RuntimeAvailability(
|
||||
IsAvailable: true,
|
||||
@@ -27,6 +27,14 @@ public static class WebView2RuntimeProbe
|
||||
Message: string.Empty);
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return new WebView2RuntimeAvailability(
|
||||
IsAvailable: false,
|
||||
Version: null,
|
||||
Message: "Embedded browser is currently unavailable on this platform.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var version = TryGetVersionFromWebView2Api();
|
||||
|
||||
@@ -70,11 +70,25 @@
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|NumberBox">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Style Selector="ui|FANumberBox">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}" />
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusMd}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|FANumberBox /template/ Button#PART_SpinUp">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="4,4,0,0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|FANumberBox /template/ Button#PART_SpinDown">
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="0,0,4,4" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="CheckBox">
|
||||
@@ -125,7 +139,7 @@
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector=".settings-scope ui|NumberBox">
|
||||
<Style Selector=".settings-scope ui|FANumberBox">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
</Style>
|
||||
@@ -152,7 +166,7 @@
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector=".settings-scope ui|NavigationView, .settings-scope ui|NavigationViewItem, .settings-scope ui|SettingsExpander, .settings-scope ui|InfoBar, .settings-scope ListBoxItem">
|
||||
<Style Selector=".settings-scope ui|FANavigationView, .settings-scope ui|FANavigationViewItem, .settings-scope ui|FASettingsExpander, .settings-scope ui|FAInfoBar, .settings-scope ListBoxItem">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
@@ -169,9 +183,9 @@
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
半透明表面样式类
|
||||
注意:这些样式使用纯色半透明画刷模拟玻璃效果,并非真正的 Mica/Acrylic 模糊材质。
|
||||
真正的 Mica/Acrylic 效果仅通过 WindowTransparencyLevel 在独立窗口上应用。
|
||||
鍗婇€忔槑琛ㄩ潰鏍峰紡绫?
|
||||
娉ㄦ剰锛氳繖浜涙牱寮忎娇鐢ㄧ函鑹插崐閫忔槑鐢诲埛妯℃嫙鐜荤拑鏁堟灉锛屽苟闈炵湡姝g殑 Mica/Acrylic 妯$硦鏉愯川銆?
|
||||
鐪熸鐨?Mica/Acrylic 鏁堟灉浠呴€氳繃 WindowTransparencyLevel 鍦ㄧ嫭绔嬬獥鍙d笂搴旂敤銆?
|
||||
-->
|
||||
|
||||
<Style Selector="Border.surface-translucent-panel">
|
||||
@@ -221,7 +235,7 @@
|
||||
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassOverlayOpacity}" />
|
||||
</Style>
|
||||
|
||||
<!-- 向后兼容的旧样式类(已弃用) -->
|
||||
<!-- 鍚戝悗鍏煎鐨勬棫鏍峰紡绫伙紙宸插純鐢級 -->
|
||||
<Style Selector="Border.glass-panel">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassPanelBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent">
|
||||
xmlns:fi="using:FluentIcons.Avalonia">
|
||||
|
||||
<Styles.Resources>
|
||||
<x:Double x:Key="PaneToggleButtonWidth">40</x:Double>
|
||||
@@ -115,7 +115,7 @@
|
||||
<Setter Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|NavigationView.settings-navigation-view">
|
||||
<Style Selector="ui|FANavigationView.settings-navigation-view">
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Opacity" Duration="0:0:0.2" Easing="0.05,0.75,0.10,1.00" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|NavigationView.settings-navigation-view /template/ Border#NavigationViewBorder">
|
||||
<Style Selector="ui|FANavigationView.settings-navigation-view /template/ Border#NavigationViewBorder">
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<BrushTransition Property="Background" Duration="0:0:0.167" Easing="0.05,0.75,0.10,1.00" />
|
||||
@@ -131,7 +131,7 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|NavigationViewItem.settings-nav-item">
|
||||
<Style Selector="ui|FANavigationViewItem.settings-nav-item">
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<BrushTransition Property="Background" Duration="0:0:0.083" Easing="0.05,0.75,0.10,1.00" />
|
||||
@@ -140,11 +140,11 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|NavigationViewItem.settings-nav-item:pointerover">
|
||||
<Style Selector="ui|FANavigationViewItem.settings-nav-item:pointerover">
|
||||
<Setter Property="RenderTransform" Value="scale(1.01)" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|NavigationViewItem.settings-nav-item:pressed">
|
||||
<Style Selector="ui|FANavigationViewItem.settings-nav-item:pressed">
|
||||
<Setter Property="RenderTransform" Value="scale(0.99)" />
|
||||
</Style>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:behaviors="using:LanMountainDesktop.Behaviors">
|
||||
|
||||
<Style Selector="StackPanel.settings-page-container">
|
||||
@@ -162,59 +162,59 @@
|
||||
<Setter Property="ColumnSpacing" Value="12" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander">
|
||||
<Style Selector="ui|FASettingsExpander">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander ComboBox, ui|SettingsExpander TextBox, ui|SettingsExpander NumericUpDown">
|
||||
<Style Selector="ui|FASettingsExpander ComboBox, ui|FASettingsExpander TextBox, ui|FASettingsExpander NumericUpDown">
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpanderItem ComboBox, ui|SettingsExpanderItem TextBox, ui|SettingsExpanderItem NumericUpDown">
|
||||
<Style Selector="ui|FASettingsExpanderItem ComboBox, ui|FASettingsExpanderItem TextBox, ui|FASettingsExpanderItem NumericUpDown">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander ToggleSwitch, ui|SettingsExpanderItem ToggleSwitch">
|
||||
<Style Selector="ui|FASettingsExpander ToggleSwitch, ui|FASettingsExpanderItem ToggleSwitch">
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card">
|
||||
<Style Selector="ui|FASettingsExpander.settings-expander-card">
|
||||
<Setter Property="Margin" Value="0,0,0,14" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card /template/ ContentPresenter#FooterContentPresenter">
|
||||
<Style Selector="ui|FASettingsExpander.settings-expander-card /template/ ContentPresenter#FooterContentPresenter">
|
||||
<Setter Property="Margin" Value="0,6,0,2" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card /template/ ContentPresenter#ContentPresenter">
|
||||
<Style Selector="ui|FASettingsExpander.settings-expander-card /template/ ContentPresenter#ContentPresenter">
|
||||
<Setter Property="Margin" Value="0,14,0,0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card ComboBox, .settings-section-card ComboBox, .settings-option-card ComboBox">
|
||||
<Style Selector="ui|FASettingsExpander.settings-expander-card ComboBox, .settings-section-card ComboBox, .settings-option-card ComboBox">
|
||||
<Setter Property="MinWidth" Value="220" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card TextBox, .settings-section-card TextBox, .settings-option-card TextBox">
|
||||
<Style Selector="ui|FASettingsExpander.settings-expander-card TextBox, .settings-section-card TextBox, .settings-option-card TextBox">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="MinHeight" Value="38" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card NumericUpDown, .settings-section-card NumericUpDown, .settings-option-card NumericUpDown">
|
||||
<Style Selector="ui|FASettingsExpander.settings-expander-card NumericUpDown, .settings-section-card NumericUpDown, .settings-option-card NumericUpDown">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="MinHeight" Value="38" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card ToggleSwitch, .settings-option-card ToggleSwitch, .settings-list-item ToggleSwitch">
|
||||
<Style Selector="ui|FASettingsExpander.settings-expander-card ToggleSwitch, .settings-option-card ToggleSwitch, .settings-list-item ToggleSwitch">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="OnContent" Value="{x:Null}" />
|
||||
<Setter Property="OffContent" Value="{x:Null}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector=".settings-section-card Button, .settings-option-card Button, .settings-list-item Button, ui|SettingsExpander.settings-expander-card Button">
|
||||
<Style Selector=".settings-section-card Button, .settings-option-card Button, .settings-list-item Button, ui|FASettingsExpander.settings-expander-card Button">
|
||||
<Setter Property="MinHeight" Value="36" />
|
||||
<Setter Property="Padding" Value="14,8" />
|
||||
</Style>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using LanMountainDesktop.Services;
|
||||
using Avalonia.Controls;
|
||||
using FluentIcons.Common;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
@@ -55,33 +54,20 @@ public sealed class ComponentLibraryCategoryViewModel
|
||||
public sealed class ComponentLibraryItemViewModel
|
||||
: ObservableObject
|
||||
{
|
||||
private readonly string _loadingPreviewText;
|
||||
private readonly string _previewUnavailableText;
|
||||
private string _displayName;
|
||||
private string? _description;
|
||||
private ComponentPreviewKey _previewKey;
|
||||
private ComponentPreviewImageEntry? _previewImageEntry;
|
||||
private ComponentPreviewImageState _previewState;
|
||||
private string? _previewErrorMessage;
|
||||
private string _previewStatusText;
|
||||
private Control? _previewControl;
|
||||
|
||||
public ComponentLibraryItemViewModel(
|
||||
string componentId,
|
||||
string displayName,
|
||||
ComponentPreviewKey previewKey,
|
||||
string? description = null,
|
||||
string loadingPreviewText = "Loading preview...",
|
||||
string previewUnavailableText = "Preview unavailable",
|
||||
ComponentPreviewImageEntry? previewImageEntry = null)
|
||||
Control? previewControl = null)
|
||||
{
|
||||
ComponentId = componentId;
|
||||
_displayName = displayName;
|
||||
_description = description;
|
||||
_previewKey = previewKey;
|
||||
_loadingPreviewText = loadingPreviewText;
|
||||
_previewUnavailableText = previewUnavailableText;
|
||||
_previewStatusText = loadingPreviewText;
|
||||
UpdatePreviewImageEntry(previewImageEntry, raiseEntryChanged: false);
|
||||
_previewControl = previewControl;
|
||||
}
|
||||
|
||||
public string ComponentId { get; }
|
||||
@@ -98,98 +84,10 @@ public sealed class ComponentLibraryItemViewModel
|
||||
set => SetProperty(ref _description, value);
|
||||
}
|
||||
|
||||
public ComponentPreviewKey PreviewKey
|
||||
public Control? PreviewControl
|
||||
{
|
||||
get => _previewKey;
|
||||
set => SetProperty(ref _previewKey, value);
|
||||
get => _previewControl;
|
||||
set => SetProperty(ref _previewControl, value);
|
||||
}
|
||||
|
||||
public ComponentPreviewImageEntry? PreviewImageEntry => _previewImageEntry;
|
||||
|
||||
public object? PreviewBitmap => _previewImageEntry?.Bitmap;
|
||||
|
||||
public ComponentPreviewImageState PreviewState => _previewState;
|
||||
|
||||
public bool IsPreviewPending => _previewState == ComponentPreviewImageState.Pending;
|
||||
|
||||
public bool IsPreviewReady => _previewState == ComponentPreviewImageState.Ready && _previewImageEntry?.Bitmap is not null;
|
||||
|
||||
public bool IsPreviewFailed => _previewState == ComponentPreviewImageState.Failed;
|
||||
|
||||
public string? PreviewErrorMessage => _previewErrorMessage;
|
||||
|
||||
public string PreviewStatusText => _previewStatusText;
|
||||
|
||||
public void UpdatePreviewImageEntry(ComponentPreviewImageEntry? previewImageEntry)
|
||||
{
|
||||
UpdatePreviewImageEntry(previewImageEntry, raiseEntryChanged: true);
|
||||
}
|
||||
|
||||
private void UpdatePreviewImageEntry(ComponentPreviewImageEntry? previewImageEntry, bool raiseEntryChanged)
|
||||
{
|
||||
if (raiseEntryChanged && ReferenceEquals(_previewImageEntry, previewImageEntry))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_previewImageEntry is not null)
|
||||
{
|
||||
_previewImageEntry.PropertyChanged -= OnPreviewImageEntryPropertyChanged;
|
||||
}
|
||||
|
||||
_previewImageEntry = previewImageEntry;
|
||||
_previewState = previewImageEntry?.State ?? ComponentPreviewImageState.Pending;
|
||||
_previewErrorMessage = previewImageEntry?.ErrorMessage;
|
||||
|
||||
_previewStatusText = _previewState switch
|
||||
{
|
||||
ComponentPreviewImageState.Ready => string.Empty,
|
||||
ComponentPreviewImageState.Failed => string.IsNullOrWhiteSpace(_previewErrorMessage)
|
||||
? _previewUnavailableText
|
||||
: _previewErrorMessage!,
|
||||
_ => _loadingPreviewText
|
||||
};
|
||||
|
||||
if (_previewImageEntry is not null)
|
||||
{
|
||||
_previewImageEntry.PropertyChanged += OnPreviewImageEntryPropertyChanged;
|
||||
}
|
||||
|
||||
RaisePreviewDependentProperties();
|
||||
}
|
||||
|
||||
private void OnPreviewImageEntryPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
if (string.IsNullOrWhiteSpace(e.PropertyName) ||
|
||||
e.PropertyName is nameof(ComponentPreviewImageEntry.Bitmap) or
|
||||
nameof(ComponentPreviewImageEntry.State) or
|
||||
nameof(ComponentPreviewImageEntry.ErrorMessage))
|
||||
{
|
||||
_previewState = _previewImageEntry?.State ?? ComponentPreviewImageState.Pending;
|
||||
_previewErrorMessage = _previewImageEntry?.ErrorMessage;
|
||||
_previewStatusText = _previewState switch
|
||||
{
|
||||
ComponentPreviewImageState.Ready => string.Empty,
|
||||
ComponentPreviewImageState.Failed => string.IsNullOrWhiteSpace(_previewErrorMessage)
|
||||
? _previewUnavailableText
|
||||
: _previewErrorMessage!,
|
||||
_ => _loadingPreviewText
|
||||
};
|
||||
|
||||
RaisePreviewDependentProperties();
|
||||
}
|
||||
}
|
||||
|
||||
private void RaisePreviewDependentProperties()
|
||||
{
|
||||
OnPropertyChanged(nameof(PreviewImageEntry));
|
||||
OnPropertyChanged(nameof(PreviewBitmap));
|
||||
OnPropertyChanged(nameof(PreviewState));
|
||||
OnPropertyChanged(nameof(IsPreviewPending));
|
||||
OnPropertyChanged(nameof(IsPreviewReady));
|
||||
OnPropertyChanged(nameof(IsPreviewFailed));
|
||||
OnPropertyChanged(nameof(PreviewErrorMessage));
|
||||
OnPropertyChanged(nameof(PreviewStatusText));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:fa="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fa="clr-namespace:FluentIcons.Avalonia;assembly=FluentIcons.Avalonia"
|
||||
xmlns:mi="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"
|
||||
mc:Ignorable="d"
|
||||
@@ -16,7 +16,7 @@
|
||||
CanResize="True"
|
||||
SizeToContent="Manual"
|
||||
ShowInTaskbar="False"
|
||||
SystemDecorations="BorderOnly"
|
||||
WindowDecorations="BorderOnly"
|
||||
Background="Transparent"
|
||||
Title="Component Editor">
|
||||
<Window.Resources>
|
||||
|
||||
@@ -60,17 +60,13 @@ public partial class ComponentEditorWindow : Window
|
||||
if (preferSystemChrome)
|
||||
{
|
||||
ExtendClientAreaToDecorationsHint = true;
|
||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
|
||||
ExtendClientAreaTitleBarHeightHint = -1;
|
||||
SystemDecorations = SystemDecorations.Full;
|
||||
WindowDecorations = WindowDecorations.Full;
|
||||
CustomTitleBarHost.IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
SystemDecorations = SystemDecorations.BorderOnly;
|
||||
WindowDecorations = WindowDecorations.BorderOnly;
|
||||
ExtendClientAreaToDecorationsHint = true;
|
||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
|
||||
ExtendClientAreaTitleBarHeightHint = 52;
|
||||
CustomTitleBarHost.IsVisible = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,13 +41,11 @@
|
||||
ColumnSpacing="4">
|
||||
<ToggleButton x:Name="TickRadioButton"
|
||||
Classes="component-editor-segmented-choice"
|
||||
Checked="OnSecondHandChanged"
|
||||
Unchecked="OnSecondHandChanged" />
|
||||
IsCheckedChanged="OnSecondHandChanged" />
|
||||
<ToggleButton x:Name="SweepRadioButton"
|
||||
Grid.Column="1"
|
||||
Classes="component-editor-segmented-choice"
|
||||
Checked="OnSecondHandChanged"
|
||||
Unchecked="OnSecondHandChanged" />
|
||||
IsCheckedChanged="OnSecondHandChanged" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
x:Class="LanMountainDesktop.Views.ComponentEditors.ShortcutComponentEditor"
|
||||
@@ -20,7 +20,7 @@
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBox Text="{Binding TargetPath}"
|
||||
IsReadOnly="True"
|
||||
Watermark="{Binding TargetPathPlaceholder}"
|
||||
PlaceholderText="{Binding TargetPathPlaceholder}"
|
||||
Grid.Column="0" />
|
||||
<Button Content="{Binding BrowseButtonText}"
|
||||
Click="OnBrowseClick"
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
<ToggleSwitch x:Name="EnabledToggleSwitch"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Checked="OnEnabledChanged"
|
||||
Unchecked="OnEnabledChanged" />
|
||||
IsCheckedChanged="OnEnabledChanged" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -77,13 +77,11 @@
|
||||
ColumnSpacing="4">
|
||||
<ToggleButton x:Name="TickRadioButton"
|
||||
Classes="component-editor-segmented-choice"
|
||||
Checked="OnSecondHandChanged"
|
||||
Unchecked="OnSecondHandChanged" />
|
||||
IsCheckedChanged="OnSecondHandChanged" />
|
||||
<ToggleButton x:Name="SweepRadioButton"
|
||||
Grid.Column="1"
|
||||
Classes="component-editor-segmented-choice"
|
||||
Checked="OnSecondHandChanged"
|
||||
Unchecked="OnSecondHandChanged" />
|
||||
IsCheckedChanged="OnSecondHandChanged" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
MinWidth="760"
|
||||
MinHeight="500"
|
||||
CanResize="True"
|
||||
SystemDecorations="Full"
|
||||
WindowDecorations="Full"
|
||||
Title="Component Library"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
||||
|
||||
@@ -99,48 +99,11 @@
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
Padding="8">
|
||||
<Grid>
|
||||
<Image Source="{Binding PreviewBitmap}"
|
||||
Stretch="Uniform"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
IsVisible="{Binding IsPreviewReady}" />
|
||||
|
||||
<Border IsVisible="{Binding IsPreviewPending}"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
||||
<StackPanel HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<ProgressBar Width="96"
|
||||
IsIndeterminate="True" />
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="{Binding PreviewStatusText}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border IsVisible="{Binding IsPreviewFailed}"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
||||
<StackPanel HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="{Binding PreviewStatusText}" />
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="{Binding PreviewErrorMessage}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
<ContentControl Content="{Binding PreviewControl}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False"
|
||||
Focusable="False" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Row="1"
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using FluentIcons.Common;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
@@ -14,10 +15,6 @@ public partial class ComponentLibraryWindow : Window
|
||||
private IComponentLibraryService? _componentLibraryService;
|
||||
private Func<double, ComponentLibraryCreateContext>? _createContextFactory;
|
||||
private Func<string, string, string>? _localize;
|
||||
private Func<ComponentLibraryComponentEntry, ComponentPreviewKey>? _previewKeyResolver;
|
||||
private Func<ComponentPreviewKey, ComponentPreviewImageEntry?>? _previewEntryResolver;
|
||||
private Action<ComponentPreviewKey>? _warmPreviewRequested;
|
||||
private Action<ComponentPreviewKey>? _renderPreviewRequested;
|
||||
private readonly ComponentLibraryWindowViewModel _viewModel = new();
|
||||
|
||||
public ComponentLibraryWindow()
|
||||
@@ -29,20 +26,12 @@ public partial class ComponentLibraryWindow : Window
|
||||
public ComponentLibraryWindow(
|
||||
IComponentLibraryService componentLibraryService,
|
||||
Func<double, ComponentLibraryCreateContext> createContextFactory,
|
||||
Func<string, string, string> localize,
|
||||
Func<ComponentLibraryComponentEntry, ComponentPreviewKey>? previewKeyResolver = null,
|
||||
Func<ComponentPreviewKey, ComponentPreviewImageEntry?>? previewEntryResolver = null,
|
||||
Action<ComponentPreviewKey>? warmPreviewRequested = null,
|
||||
Action<ComponentPreviewKey>? renderPreviewRequested = null)
|
||||
Func<string, string, string> localize)
|
||||
: this()
|
||||
{
|
||||
_componentLibraryService = componentLibraryService ?? throw new ArgumentNullException(nameof(componentLibraryService));
|
||||
_createContextFactory = createContextFactory ?? throw new ArgumentNullException(nameof(createContextFactory));
|
||||
_localize = localize ?? throw new ArgumentNullException(nameof(localize));
|
||||
_previewKeyResolver = previewKeyResolver;
|
||||
_previewEntryResolver = previewEntryResolver;
|
||||
_warmPreviewRequested = warmPreviewRequested;
|
||||
_renderPreviewRequested = renderPreviewRequested;
|
||||
Reload();
|
||||
}
|
||||
|
||||
@@ -56,6 +45,7 @@ public partial class ComponentLibraryWindow : Window
|
||||
}
|
||||
|
||||
_viewModel.Title = _localize("component_library.title", "Widgets");
|
||||
DisposePreviewControls(_viewModel.Categories.SelectMany(static category => category.Components));
|
||||
_viewModel.Categories.Clear();
|
||||
_viewModel.Components.Clear();
|
||||
|
||||
@@ -88,24 +78,12 @@ public partial class ComponentLibraryWindow : Window
|
||||
var displayName = string.IsNullOrWhiteSpace(entry.DisplayNameLocalizationKey)
|
||||
? entry.DisplayName
|
||||
: _localize?.Invoke(entry.DisplayNameLocalizationKey, entry.DisplayName) ?? entry.DisplayName;
|
||||
var previewKey = ResolvePreviewKey(entry);
|
||||
var previewEntry = _previewEntryResolver?.Invoke(previewKey);
|
||||
var item = new ComponentLibraryItemViewModel(
|
||||
var previewControl = CreateStaticPreviewControl(entry);
|
||||
return new ComponentLibraryItemViewModel(
|
||||
entry.ComponentId,
|
||||
displayName,
|
||||
previewKey,
|
||||
description: null,
|
||||
_localize?.Invoke("component_library.preview.loading", "Loading preview...") ?? "Loading preview...",
|
||||
_localize?.Invoke("component_library.preview.unavailable", "Preview unavailable") ?? "Preview unavailable",
|
||||
previewEntry);
|
||||
|
||||
if (previewEntry is null || previewEntry.State == ComponentPreviewImageState.Pending)
|
||||
{
|
||||
_warmPreviewRequested?.Invoke(previewKey);
|
||||
_renderPreviewRequested?.Invoke(previewKey);
|
||||
}
|
||||
|
||||
return item;
|
||||
previewControl);
|
||||
}
|
||||
|
||||
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
@@ -124,7 +102,7 @@ public partial class ComponentLibraryWindow : Window
|
||||
_viewModel.Components.Add(component);
|
||||
}
|
||||
|
||||
RequestPreviewWarmup(selectedCategory.Components);
|
||||
ComponentPreviewRuntimeQuiescer.Quiesce(this);
|
||||
}
|
||||
|
||||
private void OnAddComponentClick(object? sender, RoutedEventArgs e)
|
||||
@@ -147,48 +125,54 @@ public partial class ComponentLibraryWindow : Window
|
||||
Hide();
|
||||
}
|
||||
|
||||
public void UpdatePreviewImage(ComponentPreviewImageEntry previewImageEntry)
|
||||
private Control? CreateStaticPreviewControl(ComponentLibraryComponentEntry entry)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(previewImageEntry);
|
||||
|
||||
foreach (var category in _viewModel.Categories)
|
||||
if (_componentLibraryService is null || _createContextFactory is null)
|
||||
{
|
||||
foreach (var component in category.Components)
|
||||
{
|
||||
if (component.PreviewKey.Equals(previewImageEntry.Key))
|
||||
{
|
||||
component.UpdatePreviewImageEntry(previewImageEntry);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var cellSize = ResolvePreviewCellSize(entry);
|
||||
var context = _createContextFactory(cellSize) with
|
||||
{
|
||||
PlacementId = null,
|
||||
RenderMode = DesktopComponentRenderMode.LibraryPreview
|
||||
};
|
||||
|
||||
if (!_componentLibraryService.TryCreateControl(entry.ComponentId, context, out var control, out _))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (control is not null)
|
||||
{
|
||||
ComponentPreviewRuntimeQuiescer.Attach(control);
|
||||
}
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
private ComponentPreviewKey ResolvePreviewKey(ComponentLibraryComponentEntry entry)
|
||||
private static double ResolvePreviewCellSize(ComponentLibraryComponentEntry entry)
|
||||
{
|
||||
if (_previewKeyResolver is not null)
|
||||
{
|
||||
return _previewKeyResolver(entry);
|
||||
}
|
||||
|
||||
return ComponentPreviewKey.ForComponentType(entry.ComponentId, entry.MinWidthCells, entry.MinHeightCells);
|
||||
var maxWidth = 180d;
|
||||
var maxHeight = 120d;
|
||||
return Math.Clamp(
|
||||
Math.Min(
|
||||
maxWidth / Math.Max(1, entry.MinWidthCells),
|
||||
maxHeight / Math.Max(1, entry.MinHeightCells)),
|
||||
24d,
|
||||
72d);
|
||||
}
|
||||
|
||||
private void RequestPreviewWarmup(IEnumerable<ComponentLibraryItemViewModel> components)
|
||||
private static void DisposePreviewControls(IEnumerable<ComponentLibraryItemViewModel> components)
|
||||
{
|
||||
if (_warmPreviewRequested is null && _renderPreviewRequested is null)
|
||||
foreach (var control in components.Select(static component => component.PreviewControl).OfType<Control>())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var component in components)
|
||||
{
|
||||
if (!component.IsPreviewPending)
|
||||
ComponentPreviewRuntimeQuiescer.Detach(control);
|
||||
if (control is IDisposable disposable)
|
||||
{
|
||||
continue;
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
_warmPreviewRequested?.Invoke(component.PreviewKey);
|
||||
_renderPreviewRequested?.Invoke(component.PreviewKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -75,7 +75,7 @@
|
||||
Grid.Column="1"
|
||||
VerticalContentAlignment="Center"
|
||||
HorizontalContentAlignment="Left"
|
||||
Watermark="https://example.com"
|
||||
PlaceholderText="https://example.com"
|
||||
Text="https://www.bing.com"
|
||||
KeyDown="OnAddressTextBoxKeyDown" />
|
||||
|
||||
|
||||
@@ -4,11 +4,10 @@ using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Styling;
|
||||
using AvaloniaWebView;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Services;
|
||||
using WebViewCore.Events;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
@@ -28,7 +27,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
private bool _isEditMode;
|
||||
private bool _isWebViewActive = true;
|
||||
private bool _isWebViewFaulted;
|
||||
private WebView? _browserWebView;
|
||||
private NativeWebView? _browserWebView;
|
||||
private readonly WebView2RuntimeAvailability _runtimeAvailability;
|
||||
private bool _isDisposed;
|
||||
|
||||
@@ -78,7 +77,8 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
if (_browserWebView is not null)
|
||||
{
|
||||
_browserWebView.NavigationStarting -= OnBrowserWebViewNavigationStarting;
|
||||
_browserWebView.NavigationStarted -= OnBrowserWebViewNavigationStarting;
|
||||
_browserWebView.EnvironmentRequested -= OnBrowserWebViewEnvironmentRequested;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,15 +294,32 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBrowserWebViewNavigationStarting(object? sender, WebViewUrlLoadingEventArg e)
|
||||
private void OnBrowserWebViewNavigationStarting(object? sender, WebViewNavigationStartingEventArgs e)
|
||||
{
|
||||
if (e.Url is null)
|
||||
if (e.Request is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastKnownUri = e.Url;
|
||||
AddressTextBox.Text = e.Url.ToString();
|
||||
_lastKnownUri = e.Request;
|
||||
AddressTextBox.Text = e.Request.ToString();
|
||||
}
|
||||
|
||||
private void OnBrowserWebViewEnvironmentRequested(object? sender, WebViewEnvironmentRequestedEventArgs e)
|
||||
{
|
||||
if (e is not WindowsWebView2EnvironmentRequestedEventArgs windowsArgs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
windowsArgs.UserDataFolder = WebView2RuntimeProbe.ResolveUserDataFolder();
|
||||
}
|
||||
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
||||
{
|
||||
AppLogger.Warn("WebView2", "Failed to configure the WebView2 user data folder for BrowserWidget.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateWebViewActiveState()
|
||||
@@ -358,6 +375,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
GoButton.IsEnabled = true;
|
||||
AddressTextBox.IsEnabled = true;
|
||||
UnavailableOverlay.IsVisible = false;
|
||||
TryNavigate(_lastKnownUri, "ActivateWebView");
|
||||
}
|
||||
|
||||
private void DeactivateWebView(bool clearUrl)
|
||||
@@ -383,8 +401,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
try
|
||||
{
|
||||
_browserWebView.Reload();
|
||||
return true;
|
||||
return _browserWebView.Refresh();
|
||||
}
|
||||
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
||||
{
|
||||
@@ -402,7 +419,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
try
|
||||
{
|
||||
_browserWebView.Url = uri;
|
||||
_browserWebView.Navigate(uri);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
||||
@@ -421,7 +438,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
try
|
||||
{
|
||||
_browserWebView.Url = null;
|
||||
_browserWebView.Navigate(new Uri("about:blank"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -466,12 +483,14 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
return;
|
||||
}
|
||||
|
||||
_browserWebView = new WebView
|
||||
_browserWebView = new NativeWebView
|
||||
{
|
||||
Source = new Uri("about:blank"),
|
||||
IsVisible = false,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
_browserWebView.NavigationStarting += OnBrowserWebViewNavigationStarting;
|
||||
_browserWebView.NavigationStarted += OnBrowserWebViewNavigationStarting;
|
||||
_browserWebView.EnvironmentRequested += OnBrowserWebViewEnvironmentRequested;
|
||||
WebViewPresenter.Children.Insert(0, _browserWebView);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
|
||||
@@ -24,7 +24,8 @@ public sealed record DesktopComponentControlFactoryContext(
|
||||
ISettingsService SettingsService,
|
||||
IComponentInstanceSettingsStore ComponentSettingsStore,
|
||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||
string? PlacementId = null);
|
||||
string? PlacementId = null,
|
||||
DesktopComponentRenderMode RenderMode = DesktopComponentRenderMode.Live);
|
||||
|
||||
public sealed class DesktopComponentRuntimeRegistration
|
||||
{
|
||||
@@ -115,7 +116,8 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
IRecommendationInfoService recommendationInfoService,
|
||||
ICalculatorDataService calculatorDataService,
|
||||
ISettingsFacadeService settingsFacade,
|
||||
string? placementId = null)
|
||||
string? placementId = null,
|
||||
DesktopComponentRenderMode renderMode = DesktopComponentRenderMode.Live)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settingsFacade);
|
||||
|
||||
@@ -141,7 +143,8 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
settingsService,
|
||||
componentSettingsStore,
|
||||
componentAccessor,
|
||||
placementId));
|
||||
placementId,
|
||||
renderMode));
|
||||
var runtimeContext = new DesktopComponentRuntimeContext(
|
||||
Definition.Id,
|
||||
placementId,
|
||||
@@ -150,7 +153,8 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
appearanceTheme,
|
||||
chromeContext,
|
||||
componentAccessor,
|
||||
componentSettingsStore);
|
||||
componentSettingsStore,
|
||||
renderMode);
|
||||
|
||||
ApplySettingsDependencies(control, settingsService, componentSettingsStore);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:symbol="using:FluentIcons.Common"
|
||||
x:Class="LanMountainDesktop.Views.Components.NotificationBoxWidget">
|
||||
|
||||
@@ -9,18 +9,18 @@
|
||||
Background="Transparent"
|
||||
ClipToBounds="True">
|
||||
<Grid>
|
||||
<!-- 主卡片 -->
|
||||
<!-- 涓诲崱鐗?-->
|
||||
<Border x:Name="CardBorder"
|
||||
Background="#FCFCFD"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||
Padding="12,10">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<!-- 头部 -->
|
||||
<!-- 澶撮儴 -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||
<fi:SymbolIcon x:Name="HeaderIcon" Symbol="{x:Static symbol:Symbol.MailInbox}" FontSize="16" />
|
||||
<TextBlock x:Name="HeaderTextBlock"
|
||||
Text="消息盒子"
|
||||
Text="娑堟伅鐩掑瓙"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold" />
|
||||
<Border x:Name="UnreadBadge"
|
||||
@@ -46,24 +46,24 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- 通知列表 -->
|
||||
<!-- 閫氱煡鍒楄〃 -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
Margin="0,8,0,0"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="NotificationListPanel" Spacing="6" />
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<!-- 绌虹姸鎬?-->
|
||||
<TextBlock x:Name="EmptyStateText"
|
||||
Grid.Row="1"
|
||||
Text="暂无通知"
|
||||
Text="鏆傛棤閫氱煡"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#8B95A5"
|
||||
FontSize="13"
|
||||
IsVisible="False" />
|
||||
|
||||
<!-- 隐私模式遮罩 -->
|
||||
<!-- 闅愮妯″紡閬僵 -->
|
||||
<Border x:Name="PrivacyOverlay"
|
||||
Grid.Row="1"
|
||||
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
|
||||
@@ -73,13 +73,13 @@
|
||||
VerticalAlignment="Center"
|
||||
Spacing="6">
|
||||
<fi:SymbolIcon Symbol="{x:Static symbol:Symbol.EyeOff}" FontSize="24" Foreground="#8B95A5" />
|
||||
<TextBlock Text="您有新的通知"
|
||||
<TextBlock Text="鎮ㄦ湁鏂扮殑閫氱煡"
|
||||
Foreground="#8B95A5"
|
||||
FontSize="12" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 底部状态 -->
|
||||
<!-- 搴曢儴鐘舵€?-->
|
||||
<TextBlock x:Name="StatusTextBlock"
|
||||
Grid.Row="2"
|
||||
FontSize="11"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
@@ -72,7 +72,7 @@
|
||||
TextWrapping="Wrap" />
|
||||
<TextBox x:Name="DialogRenameTextBox"
|
||||
IsVisible="False"
|
||||
Watermark="Enter session name"
|
||||
PlaceholderText="Enter session name"
|
||||
MinWidth="120"
|
||||
VerticalContentAlignment="Center" />
|
||||
<Grid ColumnDefinitions="*,*"
|
||||
|
||||
@@ -577,7 +577,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
CultureInfo.InvariantCulture,
|
||||
L("study.session_history.dialog.rename_message", "Set a new name for \"{0}\"."),
|
||||
label);
|
||||
DialogRenameTextBox.Watermark = L("study.session_history.rename_placeholder", "Enter session name");
|
||||
DialogRenameTextBox.PlaceholderText = L("study.session_history.rename_placeholder", "Enter session name");
|
||||
if (string.IsNullOrWhiteSpace(DialogRenameTextBox.Text))
|
||||
{
|
||||
DialogRenameTextBox.Text = label;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
x:Class="LanMountainDesktop.Views.DesktopWidgetWindow"
|
||||
Title="Desktop Component"
|
||||
ShowInTaskbar="False"
|
||||
SystemDecorations="None"
|
||||
WindowDecorations="None"
|
||||
Background="Transparent"
|
||||
Topmost="False"
|
||||
SizeToContent="WidthAndHeight"
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:converters="using:Avalonia.Data.Converters"
|
||||
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryControl"
|
||||
x:DataType="vm:ComponentLibraryWindowViewModel">
|
||||
|
||||
<UserControl.Styles>
|
||||
<!-- 分类列表项样式 - 遵循 Fluent NavigationView 风格 -->
|
||||
<Style Selector="ListBoxItem.category-item">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Margin" Value="0,2"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}"/>
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<BrushTransition Property="Background" Duration="0:0:0.083" Easing="0.05,0.75,0.10,1.00"/>
|
||||
</Transitions>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem.category-item:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}"/>
|
||||
@@ -26,18 +18,6 @@
|
||||
<Style Selector="ListBoxItem.category-item:selected /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem.category-item:pressed /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource SubtleFillColorTertiaryBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- 分类项图标和文字 -->
|
||||
<Style Selector="ListBoxItem.category-item fi|FluentIcon.category-icon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem.category-item:selected fi|FluentIcon.category-icon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style Selector="ListBoxItem.category-item TextBlock.category-text">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}"/>
|
||||
</Style>
|
||||
@@ -47,14 +27,9 @@
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="0"
|
||||
Margin="0">
|
||||
<!-- 左侧导航列 - 分类列表 + 底部"查找更多组件" -->
|
||||
<Border Width="280"
|
||||
Background="Transparent">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<Border Width="280" Background="Transparent">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<!-- 分类列表 -->
|
||||
<ListBox x:Name="CategoryListBox"
|
||||
Grid.Row="0"
|
||||
Background="Transparent"
|
||||
@@ -64,13 +39,10 @@
|
||||
ItemsSource="{Binding Categories}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="12"
|
||||
Margin="12,10">
|
||||
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12" Margin="12,10">
|
||||
<fi:FluentIcon Icon="{Binding Icon}"
|
||||
IconVariant="Regular"
|
||||
FontSize="18"
|
||||
Classes="category-icon"/>
|
||||
FontSize="18"/>
|
||||
<TextBlock Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
@@ -81,9 +53,7 @@
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- 底部"查找更多组件" - 在左侧导航列底部 -->
|
||||
<StackPanel Grid.Row="1"
|
||||
Margin="12,8,8,12">
|
||||
<StackPanel Grid.Row="1" Margin="12,8,8,12">
|
||||
<Border Height="1"
|
||||
Background="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
Opacity="0.4"
|
||||
@@ -93,42 +63,33 @@
|
||||
Click="OnFindMoreComponentsClick">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<fi:FluentIcon Icon="Globe" IconVariant="Regular" FontSize="14"/>
|
||||
<TextBlock Text="查找更多组件"/>
|
||||
<TextBlock Text="Find More Components"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 右侧内容区与左侧的分隔线 -->
|
||||
<Border Grid.Column="1"
|
||||
Width="1"
|
||||
HorizontalAlignment="Left"
|
||||
Background="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
Opacity="0.5"/>
|
||||
|
||||
<!-- 组件预览区 (右侧) -->
|
||||
<ScrollViewer Grid.Column="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="16,8,12,8"
|
||||
Spacing="0">
|
||||
|
||||
<!-- 有选中组件时的显示 -->
|
||||
<StackPanel Margin="16,8,12,8">
|
||||
<Panel IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNotNull}}">
|
||||
|
||||
<!-- 组件展示面板 - 有独立背景色,与窗口背景形成层级分界 -->
|
||||
<Border Classes="surface-translucent-panel"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="16">
|
||||
<!-- 组件标题 -->
|
||||
<TextBlock FontSize="28"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="{Binding SelectedComponent.DisplayName}"/>
|
||||
|
||||
<!-- 固定大小的预览卡片 -->
|
||||
<Border CornerRadius="{DynamicResource DesignCornerRadiusSm}"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
@@ -136,59 +97,14 @@
|
||||
Width="420"
|
||||
Height="300"
|
||||
HorizontalAlignment="Center">
|
||||
<Grid Margin="16">
|
||||
<!-- 预览图片 -->
|
||||
<Image Source="{Binding SelectedComponent.PreviewBitmap}"
|
||||
Stretch="Uniform"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality"
|
||||
IsVisible="{Binding SelectedComponent.IsPreviewReady}"/>
|
||||
|
||||
<!-- 加载中状态 -->
|
||||
<Border IsVisible="{Binding SelectedComponent.IsPreviewPending}"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
||||
<StackPanel HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="12">
|
||||
<ProgressBar Width="120"
|
||||
IsIndeterminate="True"/>
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="{Binding SelectedComponent.PreviewStatusText}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 失败状态 -->
|
||||
<Border IsVisible="{Binding SelectedComponent.IsPreviewFailed}"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
||||
<StackPanel HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<fi:FluentIcon Icon="ImageOff"
|
||||
IconVariant="Regular"
|
||||
FontSize="48"
|
||||
Opacity="0.5"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"/>
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="{Binding SelectedComponent.PreviewStatusText}"/>
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="{Binding SelectedComponent.PreviewErrorMessage}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
<ContentControl x:Name="SelectedComponentPreviewHost"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False"
|
||||
Focusable="False"/>
|
||||
</Border>
|
||||
|
||||
<!-- "添加小组件"按钮 - 在面板内居中,使用主题强调色 -->
|
||||
<Button HorizontalAlignment="Center"
|
||||
Classes="accent"
|
||||
Padding="24,10"
|
||||
@@ -196,19 +112,19 @@
|
||||
Click="OnAddComponentClick">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<fi:FluentIcon Icon="Add" IconVariant="Regular" FontSize="16"/>
|
||||
<TextBlock Text="添加小组件" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="Add Component" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<Grid IsVisible="{Binding SelectedComponent, Converter={x:Static converters:ObjectConverters.IsNull}}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
MinHeight="400">
|
||||
<StackPanel Spacing="16" HorizontalAlignment="Center"
|
||||
<StackPanel Spacing="16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<fi:FluentIcon Icon="Apps"
|
||||
IconVariant="Regular"
|
||||
@@ -218,7 +134,7 @@
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="请从左侧选择一个组件"/>
|
||||
Text="Select a component to view its details."/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
@@ -11,7 +11,6 @@ using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
@@ -19,18 +18,19 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
||||
{
|
||||
public event EventHandler<string>? AddComponentRequested;
|
||||
|
||||
private readonly ComponentLibraryWindowViewModel _viewModel = new();
|
||||
private List<DesktopComponentDefinition> _allDefinitions = new();
|
||||
private static readonly LocalizationService LocalizationService = new();
|
||||
|
||||
private ComponentRegistry? _componentRegistry;
|
||||
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
|
||||
private readonly ComponentLibraryWindowViewModel _viewModel = new();
|
||||
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
private readonly IWeatherInfoService _weatherDataService;
|
||||
private readonly TimeZoneService _timeZoneService;
|
||||
private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService();
|
||||
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
|
||||
|
||||
private static readonly LocalizationService _localizationService = new();
|
||||
private List<DesktopComponentDefinition> _allDefinitions = new();
|
||||
private ComponentRegistry? _componentRegistry;
|
||||
private DesktopComponentRuntimeRegistry? _componentRuntimeRegistry;
|
||||
private Control? _selectedPreviewControl;
|
||||
|
||||
public FusedDesktopComponentLibraryControl()
|
||||
{
|
||||
@@ -43,10 +43,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
||||
LoadRegistry();
|
||||
LoadCategories();
|
||||
|
||||
// 为 ListBoxItem 添加 category-item 样式类
|
||||
CategoryListBox.ContainerPrepared += OnCategoryListBoxContainerPrepared;
|
||||
|
||||
// 默认选择第一个分类
|
||||
if (_viewModel.Categories.Count > 0)
|
||||
{
|
||||
CategoryListBox.SelectedIndex = 0;
|
||||
@@ -55,6 +52,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
||||
|
||||
private void OnCategoryListBoxContainerPrepared(object? sender, ContainerPreparedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
if (e.Container is ListBoxItem listBoxItem)
|
||||
{
|
||||
listBoxItem.Classes.Add("category-item");
|
||||
@@ -71,7 +69,7 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
||||
_settingsFacade);
|
||||
|
||||
_allDefinitions = _componentRegistry.GetAll()
|
||||
.Where(d => d.AllowDesktopPlacement)
|
||||
.Where(static definition => definition.AllowDesktopPlacement)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -80,8 +78,6 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
||||
_viewModel.Categories.Clear();
|
||||
|
||||
var languageCode = _settingsFacade.Region.Get().LanguageCode;
|
||||
|
||||
// 添加"全部组件"分类
|
||||
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
||||
"all",
|
||||
L(languageCode, "component_category.all", "All"),
|
||||
@@ -89,32 +85,26 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
||||
Array.Empty<ComponentLibraryItemViewModel>()));
|
||||
|
||||
var usedCategories = _allDefinitions
|
||||
.Select(d => d.Category)
|
||||
.Distinct()
|
||||
.Where(c => !string.IsNullOrEmpty(c));
|
||||
.Select(static definition => definition.Category)
|
||||
.Where(static category => !string.IsNullOrWhiteSpace(category))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var cat in usedCategories)
|
||||
foreach (var category in usedCategories)
|
||||
{
|
||||
var icon = ResolveCategoryIcon(cat);
|
||||
var title = GetLocalizedCategoryTitle(languageCode, cat);
|
||||
|
||||
var categoryComponents = _allDefinitions
|
||||
.Where(d => string.Equals(d.Category, cat, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(d => d.DisplayName)
|
||||
.Select(d => CreateComponentItem(d))
|
||||
.Where(definition => string.Equals(definition.Category, category, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(static definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(CreateComponentItem)
|
||||
.ToArray();
|
||||
|
||||
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
||||
cat,
|
||||
title,
|
||||
icon,
|
||||
category,
|
||||
GetLocalizedCategoryTitle(languageCode, category),
|
||||
ResolveCategoryIcon(category),
|
||||
categoryComponents));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分类图标映射 - 与阑山桌面 Dock 栏组件库 (MainWindow.ComponentSystem) 保持一致
|
||||
/// </summary>
|
||||
private static Symbol ResolveCategoryIcon(string categoryId)
|
||||
{
|
||||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) return Symbol.Clock;
|
||||
@@ -129,9 +119,6 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
||||
return Symbol.Apps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分类本地化标题 - 与阑山桌面 Dock 栏组件库 (MainWindow.ComponentSystem) 保持一致
|
||||
/// </summary>
|
||||
private string GetLocalizedCategoryTitle(string languageCode, string categoryId)
|
||||
{
|
||||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase)) return L(languageCode, "component_category.clock", "Clock");
|
||||
@@ -148,101 +135,123 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
||||
|
||||
private string L(string languageCode, string key, string fallback)
|
||||
{
|
||||
return _localizationService.GetString(languageCode, key, fallback);
|
||||
return LocalizationService.GetString(languageCode, key, fallback);
|
||||
}
|
||||
|
||||
private ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition)
|
||||
private static ComponentLibraryItemViewModel CreateComponentItem(DesktopComponentDefinition definition)
|
||||
{
|
||||
var previewKey = ComponentPreviewKey.ForComponentType(
|
||||
definition.Id,
|
||||
definition.MinWidthCells,
|
||||
definition.MinHeightCells);
|
||||
|
||||
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
|
||||
ComponentPreviewImageEntry? previewEntry = null;
|
||||
|
||||
if (mainWindow is not null)
|
||||
{
|
||||
previewEntry = mainWindow.GetPreviewEntry(previewKey);
|
||||
}
|
||||
|
||||
var item = new ComponentLibraryItemViewModel(
|
||||
definition.Id,
|
||||
definition.DisplayName,
|
||||
previewKey,
|
||||
description: null,
|
||||
"正在加载预览...",
|
||||
"预览不可用",
|
||||
previewEntry);
|
||||
|
||||
if (mainWindow is not null && (previewEntry is null || previewEntry.State == ComponentPreviewImageState.Pending))
|
||||
{
|
||||
mainWindow.RequestDetachedLibraryPreview(previewKey);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public void UpdatePreviewImage(ComponentPreviewImageEntry entry)
|
||||
{
|
||||
foreach (var category in _viewModel.Categories)
|
||||
{
|
||||
foreach (var component in category.Components)
|
||||
{
|
||||
if (component.PreviewKey.Equals(entry.Key))
|
||||
{
|
||||
component.UpdatePreviewImageEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ComponentLibraryItemViewModel(definition.Id, definition.DisplayName);
|
||||
}
|
||||
|
||||
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
UpdateSelectedComponent();
|
||||
}
|
||||
|
||||
private void UpdateSelectedComponent()
|
||||
{
|
||||
var selectedCategory = CategoryListBox.SelectedItem as ComponentLibraryCategoryViewModel;
|
||||
if (selectedCategory is null)
|
||||
if (CategoryListBox.SelectedItem is not ComponentLibraryCategoryViewModel selectedCategory)
|
||||
{
|
||||
_viewModel.SelectedComponent = null;
|
||||
SetSelectedPreviewControl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取该分类下的组件列表
|
||||
IEnumerable<DesktopComponentDefinition> filtered;
|
||||
if (selectedCategory.Id == "all")
|
||||
{
|
||||
filtered = _allDefinitions.OrderBy(d => d.DisplayName);
|
||||
}
|
||||
else
|
||||
{
|
||||
filtered = _allDefinitions
|
||||
.Where(d => string.Equals(d.Category, selectedCategory.Id, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(d => d.DisplayName);
|
||||
}
|
||||
var filtered = selectedCategory.Id == "all"
|
||||
? _allDefinitions.OrderBy(static definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
: _allDefinitions
|
||||
.Where(definition => string.Equals(definition.Category, selectedCategory.Id, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(static definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// 选择该分类下的第一个组件作为默认选中
|
||||
var firstComponent = filtered.FirstOrDefault();
|
||||
if (firstComponent is not null)
|
||||
{
|
||||
// 查找或创建对应的 ViewModel
|
||||
var existingComponent = selectedCategory.Components.FirstOrDefault(c => c.ComponentId == firstComponent.Id);
|
||||
if (existingComponent is not null)
|
||||
{
|
||||
_viewModel.SelectedComponent = existingComponent;
|
||||
}
|
||||
else
|
||||
{
|
||||
_viewModel.SelectedComponent = CreateComponentItem(firstComponent);
|
||||
}
|
||||
}
|
||||
else
|
||||
if (firstComponent is null)
|
||||
{
|
||||
_viewModel.SelectedComponent = null;
|
||||
SetSelectedPreviewControl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
_viewModel.SelectedComponent = selectedCategory.Components.FirstOrDefault(component => component.ComponentId == firstComponent.Id)
|
||||
?? CreateComponentItem(firstComponent);
|
||||
SetSelectedPreviewControl(CreateStaticPreviewControl(firstComponent));
|
||||
}
|
||||
|
||||
private Control? CreateStaticPreviewControl(DesktopComponentDefinition definition)
|
||||
{
|
||||
if (_componentRuntimeRegistry is null ||
|
||||
!_componentRuntimeRegistry.TryGetDescriptor(definition.Id, out var descriptor))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var control = descriptor.CreateControl(
|
||||
ResolvePreviewCellSize(definition),
|
||||
_timeZoneService,
|
||||
_weatherDataService,
|
||||
_recommendationInfoService,
|
||||
_calculatorDataService,
|
||||
_settingsFacade,
|
||||
placementId: null,
|
||||
renderMode: DesktopComponentRenderMode.LibraryPreview);
|
||||
ComponentPreviewRuntimeQuiescer.Attach(control);
|
||||
return control;
|
||||
}
|
||||
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
||||
{
|
||||
AppLogger.Warn(
|
||||
"ComponentLibrary",
|
||||
$"Failed to create static fused preview for component '{definition.Id}'.",
|
||||
ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static double ResolvePreviewCellSize(DesktopComponentDefinition definition)
|
||||
{
|
||||
const double maxWidth = 360d;
|
||||
const double maxHeight = 240d;
|
||||
return Math.Clamp(
|
||||
Math.Min(
|
||||
maxWidth / Math.Max(1, definition.MinWidthCells),
|
||||
maxHeight / Math.Max(1, definition.MinHeightCells)),
|
||||
32d,
|
||||
96d);
|
||||
}
|
||||
|
||||
private void SetSelectedPreviewControl(Control? control)
|
||||
{
|
||||
DisposeSelectedPreviewControl();
|
||||
_selectedPreviewControl = control;
|
||||
if (SelectedComponentPreviewHost is not null)
|
||||
{
|
||||
SelectedComponentPreviewHost.Content = control;
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeSelectedPreviewControl()
|
||||
{
|
||||
if (_selectedPreviewControl is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ComponentPreviewRuntimeQuiescer.Detach(_selectedPreviewControl);
|
||||
if (_selectedPreviewControl is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
_selectedPreviewControl = null;
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
DisposeSelectedPreviewControl();
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
}
|
||||
|
||||
private void OnAddComponentClick(object? sender, RoutedEventArgs e)
|
||||
@@ -255,15 +264,11 @@ public partial class FusedDesktopComponentLibraryControl : UserControl
|
||||
|
||||
private void OnFindMoreComponentsClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// 打开设置窗口并导航到插件目录页面
|
||||
if (Application.Current is App app)
|
||||
{
|
||||
app.OpenIndependentSettingsModule("FusedDesktopComponentLibrary", "plugin-catalog");
|
||||
}
|
||||
|
||||
// 关闭所在窗口
|
||||
var window = this.FindAncestorOfType<Window>();
|
||||
var componentLibraryWindow = this.FindAncestorOfType<Window>();
|
||||
componentLibraryWindow?.Close();
|
||||
this.FindAncestorOfType<Window>()?.Close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:LanMountainDesktop.Views"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Views.FusedDesktopComponentLibraryWindow"
|
||||
Width="860" Height="620"
|
||||
MinWidth="600" MinHeight="500"
|
||||
Width="860"
|
||||
Height="620"
|
||||
MinWidth="600"
|
||||
MinHeight="500"
|
||||
CanResize="True"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
SystemDecorations="BorderOnly"
|
||||
WindowDecorations="BorderOnly"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="NoChrome"
|
||||
ExtendClientAreaTitleBarHeightHint="48"
|
||||
Background="Transparent"
|
||||
Title="添加小组件">
|
||||
Title="Add Component">
|
||||
|
||||
<Grid x:Name="RootGrid"
|
||||
Classes="settings-scope"
|
||||
Background="{DynamicResource AdaptiveSettingsWindowBackgroundBrush}"
|
||||
RowDefinitions="Auto,*">
|
||||
<!-- 自定义标题栏 - 与 SettingsWindow 风格一致 -->
|
||||
<Border x:Name="WindowTitleBarHost"
|
||||
Height="48"
|
||||
Padding="12,0,12,0"
|
||||
@@ -41,14 +41,14 @@
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
IsHitTestVisible="False"
|
||||
Text="添加小组件" />
|
||||
Text="Add Component" />
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
FontSize="12"
|
||||
Opacity="0.6"
|
||||
IsHitTestVisible="False"
|
||||
VerticalAlignment="Center"
|
||||
Text="将精美组件放置在您的系统桌面上(负一屏)" />
|
||||
Text="Browse available widgets and add them to the current fused desktop layout." />
|
||||
|
||||
<Button x:Name="CloseWindowButton"
|
||||
Grid.Column="3"
|
||||
@@ -65,7 +65,6 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 组件库控件 -->
|
||||
<controls:FusedDesktopComponentLibraryControl x:Name="LibraryControl"
|
||||
Grid.Row="1"
|
||||
Margin="12,8,16,8" />
|
||||
|
||||
@@ -118,9 +118,4 @@ public partial class FusedDesktopComponentLibraryWindow : Window
|
||||
var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow;
|
||||
mainWindow?.UnregisterFusedLibraryWindow(this);
|
||||
}
|
||||
|
||||
public void UpdatePreviewImage(ComponentPreviewImageEntry entry)
|
||||
{
|
||||
LibraryControl.UpdatePreviewImage(entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,429 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class MainWindow
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private const double PreviewRenderCellSizeMin = 42;
|
||||
private const double PreviewRenderCellSizeMax = 112;
|
||||
|
||||
private readonly IComponentPreviewImageService _componentPreviewImageService = new ComponentPreviewImageService();
|
||||
private readonly Dictionary<ComponentPreviewKey, List<ComponentLibraryPreviewVisualTarget>> _componentLibraryPreviewVisualTargets = new(ComponentPreviewKeyComparer.Instance);
|
||||
private bool _componentLibraryPreviewWarmupStarted;
|
||||
private FusedDesktopComponentLibraryWindow? _fusedLibraryWindow;
|
||||
|
||||
private sealed record ComponentLibraryPreviewVisualTarget(Image Image, Control Fallback);
|
||||
|
||||
private void EnsureComponentLibraryPreviewWarmup()
|
||||
{
|
||||
if (_componentLibraryCategories.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var activeCategoryId = _componentLibraryActiveCategoryId ??
|
||||
_componentLibraryCategories[Math.Clamp(_componentLibraryCategoryIndex, 0, _componentLibraryCategories.Count - 1)].Id;
|
||||
if (!_componentLibraryPreviewWarmupStarted)
|
||||
{
|
||||
_componentLibraryPreviewWarmupStarted = true;
|
||||
_ = WarmComponentLibraryPreviewsSeriallyAsync(activeCategoryId);
|
||||
return;
|
||||
}
|
||||
|
||||
var activeCategory = _componentLibraryCategories.FirstOrDefault(category =>
|
||||
string.Equals(category.Id, activeCategoryId, StringComparison.OrdinalIgnoreCase));
|
||||
if (activeCategory is not null)
|
||||
{
|
||||
_ = WarmComponentLibraryCategoryPreviewsAsync(activeCategory);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WarmComponentLibraryPreviewsSeriallyAsync(string activeCategoryId)
|
||||
{
|
||||
var prioritized = _componentLibraryCategories
|
||||
.OrderBy(category => string.Equals(category.Id, activeCategoryId, StringComparison.OrdinalIgnoreCase) ? 0 : 1)
|
||||
.ToList();
|
||||
|
||||
foreach (var category in prioritized)
|
||||
{
|
||||
await WarmComponentLibraryCategoryPreviewsAsync(category);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WarmComponentLibraryCategoryPreviewsAsync(ComponentLibraryCategory category)
|
||||
{
|
||||
foreach (var component in category.Components)
|
||||
{
|
||||
var span = NormalizeComponentCellSpan(
|
||||
component.ComponentId,
|
||||
(component.MinWidthCells, component.MinHeightCells));
|
||||
await EnsureComponentTypePreviewImageAsync(component.ComponentId, span.WidthCells, span.HeightCells);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IImage?> EnsureComponentTypePreviewImageAsync(string componentId, int widthCells, int heightCells)
|
||||
private Control CreateStaticComponentLibraryPreview(
|
||||
string componentId,
|
||||
double cellSize,
|
||||
double previewWidth,
|
||||
double previewHeight)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(componentId))
|
||||
{
|
||||
return null;
|
||||
return CreateStaticComponentPreviewFallback(previewWidth, previewHeight);
|
||||
}
|
||||
|
||||
var key = CreateComponentTypePreviewKey(componentId, widthCells, heightCells);
|
||||
var cached = ResolvePreviewImageFromService(key);
|
||||
if (cached is not null)
|
||||
{
|
||||
ApplyPreviewEntryToEmbeddedVisuals(key);
|
||||
return cached;
|
||||
}
|
||||
var context = new ComponentLibraryCreateContext(
|
||||
cellSize,
|
||||
_timeZoneService,
|
||||
_weatherDataService,
|
||||
_recommendationInfoService,
|
||||
_calculatorDataService,
|
||||
_settingsFacade,
|
||||
PlacementId: null,
|
||||
RenderMode: DesktopComponentRenderMode.LibraryPreview);
|
||||
|
||||
var entry = await QueuePreviewGenerationAsync(
|
||||
key,
|
||||
pageIndex: null,
|
||||
action: "ComponentTypePreview",
|
||||
forceRefresh: false);
|
||||
return entry.Bitmap;
|
||||
}
|
||||
|
||||
private async Task<IImage?> RefreshPlacementPreviewImageAsync(DesktopComponentPlacementSnapshot? placement, bool forceRefresh)
|
||||
{
|
||||
if (placement is null ||
|
||||
string.IsNullOrWhiteSpace(placement.ComponentId) ||
|
||||
string.IsNullOrWhiteSpace(placement.PlacementId))
|
||||
if (!_componentLibraryService.TryCreateControl(componentId, context, out var control, out var exception) ||
|
||||
control is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!IsPlacementPresent(placement.PlacementId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var snapshot = ClonePlacementSnapshot(placement);
|
||||
var key = CreatePlacementPreviewKey(
|
||||
snapshot.ComponentId,
|
||||
snapshot.PlacementId,
|
||||
snapshot.WidthCells,
|
||||
snapshot.HeightCells);
|
||||
if (!forceRefresh)
|
||||
{
|
||||
var cached = ResolvePreviewImageFromService(key);
|
||||
if (cached is not null)
|
||||
if (exception is not null)
|
||||
{
|
||||
return cached;
|
||||
AppLogger.Warn(
|
||||
"ComponentLibrary",
|
||||
$"Failed to create static preview for component '{componentId}'.",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_componentPreviewImageService.RemovePlacementPreviews(snapshot.PlacementId);
|
||||
|
||||
return CreateStaticComponentPreviewFallback(previewWidth, previewHeight);
|
||||
}
|
||||
|
||||
var entry = await QueuePreviewGenerationAsync(
|
||||
key,
|
||||
snapshot.PageIndex,
|
||||
action: "PlacementPreview",
|
||||
forceRefresh: false);
|
||||
if (!IsPlacementPresent(snapshot.PlacementId))
|
||||
{
|
||||
RemovePlacementPreviewImage(snapshot.PlacementId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.Bitmap;
|
||||
control.Width = previewWidth;
|
||||
control.Height = previewHeight;
|
||||
ComponentPreviewRuntimeQuiescer.Attach(control);
|
||||
return control;
|
||||
}
|
||||
|
||||
private async Task<ComponentPreviewImageEntry> QueuePreviewGenerationAsync(
|
||||
ComponentPreviewKey key,
|
||||
int? pageIndex,
|
||||
string action,
|
||||
bool forceRefresh,
|
||||
CancellationToken cancellationToken = default)
|
||||
private Control CreateStaticComponentPreviewFallback(double previewWidth, double previewHeight)
|
||||
{
|
||||
var renderCellSize = ResolvePreviewRenderCellSize(key.WidthCells, key.HeightCells);
|
||||
var visualSignature = BuildPreviewVisualSignature(key, renderCellSize);
|
||||
if (forceRefresh)
|
||||
{
|
||||
_componentPreviewImageService.Invalidate(key, visualSignature);
|
||||
}
|
||||
|
||||
var entry = await _componentPreviewImageService.QueueGenerationAsync(
|
||||
key,
|
||||
visualSignature,
|
||||
async ct =>
|
||||
{
|
||||
_ = ct;
|
||||
if (key.Kind == ComponentPreviewKeyKind.PlacementInstance &&
|
||||
!IsPlacementPresent(key.PlacementId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bitmap = await CapturePreviewImageAsync(
|
||||
key.ComponentTypeId,
|
||||
key.PlacementId,
|
||||
pageIndex,
|
||||
key.WidthCells,
|
||||
key.HeightCells,
|
||||
renderCellSize,
|
||||
action);
|
||||
if (key.Kind == ComponentPreviewKeyKind.PlacementInstance &&
|
||||
!IsPlacementPresent(key.PlacementId))
|
||||
{
|
||||
DisposeImageIfNeeded(bitmap);
|
||||
return null;
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
},
|
||||
cancellationToken);
|
||||
NotifyPreviewEntryUpdated(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
private async Task<IImage?> CapturePreviewImageAsync(
|
||||
string componentId,
|
||||
string? placementId,
|
||||
int? pageIndex,
|
||||
int widthCells,
|
||||
int heightCells,
|
||||
double renderCellSize,
|
||||
string action)
|
||||
{
|
||||
if (ComponentPreviewStagingHost is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var safeWidthCells = Math.Max(1, widthCells);
|
||||
var safeHeightCells = Math.Max(1, heightCells);
|
||||
var safeCellSize = Math.Clamp(renderCellSize, PreviewRenderCellSizeMin, PreviewRenderCellSizeMax);
|
||||
var previewWidth = safeWidthCells * safeCellSize;
|
||||
var previewHeight = safeHeightCells * safeCellSize;
|
||||
|
||||
var previewControl = CreateDesktopComponentControl(
|
||||
componentId,
|
||||
safeCellSize,
|
||||
placementId,
|
||||
pageIndex,
|
||||
action);
|
||||
if (previewControl is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
previewControl.IsHitTestVisible = false;
|
||||
previewControl.Focusable = false;
|
||||
|
||||
var stage = new Border
|
||||
return new Border
|
||||
{
|
||||
Width = previewWidth,
|
||||
Height = previewHeight,
|
||||
Background = Brushes.Transparent,
|
||||
ClipToBounds = true,
|
||||
Child = previewControl
|
||||
Background = GetThemeBrush("AdaptiveCardBackgroundBrush"),
|
||||
BorderBrush = GetThemeBrush("AdaptiveButtonBorderBrush"),
|
||||
BorderThickness = new Avalonia.Thickness(1),
|
||||
CornerRadius = new Avalonia.CornerRadius(Math.Clamp(Math.Min(previewWidth, previewHeight) * 0.18, 12, 28)),
|
||||
IsHitTestVisible = false,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = L("component_library.preview_unavailable", "Preview unavailable"),
|
||||
FontSize = 11,
|
||||
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"),
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Canvas.SetLeft(stage, -20000);
|
||||
Canvas.SetTop(stage, -20000);
|
||||
ComponentPreviewStagingHost.Children.Add(stage);
|
||||
|
||||
try
|
||||
private static void DisposeStaticComponentLibraryPreviews(IEnumerable<Control> roots)
|
||||
{
|
||||
foreach (var control in roots.SelectMany(EnumerateControls))
|
||||
{
|
||||
stage.Measure(new Size(previewWidth, previewHeight));
|
||||
stage.Arrange(new Rect(0, 0, previewWidth, previewHeight));
|
||||
stage.UpdateLayout();
|
||||
await WaitForPreviewRenderPassAsync();
|
||||
|
||||
var renderScale = RenderScaling > 0 ? RenderScaling : 1d;
|
||||
var pixelSize = new PixelSize(
|
||||
Math.Max(1, (int)Math.Ceiling(previewWidth * renderScale)),
|
||||
Math.Max(1, (int)Math.Ceiling(previewHeight * renderScale)));
|
||||
var bitmap = new RenderTargetBitmap(pixelSize, new Vector(96 * renderScale, 96 * renderScale));
|
||||
bitmap.Render(stage);
|
||||
return bitmap;
|
||||
}
|
||||
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
|
||||
{
|
||||
AppLogger.Warn(
|
||||
"ComponentPreview",
|
||||
$"Action={action}; ComponentId={componentId}; PlacementId={placementId ?? string.Empty}; ExceptionType={ex.GetType().FullName}; IsFatal=false",
|
||||
ex);
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ComponentPreviewStagingHost.Children.Remove(stage);
|
||||
ClearTimeZoneServiceBindings(stage);
|
||||
if (previewControl is IDisposable disposableControl)
|
||||
ComponentPreviewRuntimeQuiescer.Detach(control);
|
||||
if (control is IDisposable disposable)
|
||||
{
|
||||
disposableControl.Dispose();
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WaitForPreviewRenderPassAsync()
|
||||
private static IEnumerable<Control> EnumerateControls(Control root)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(static () => { }, DispatcherPriority.Background);
|
||||
await Dispatcher.UIThread.InvokeAsync(static () => { }, DispatcherPriority.Render);
|
||||
}
|
||||
yield return root;
|
||||
|
||||
private double ResolvePreviewRenderCellSize(int widthCells, int heightCells)
|
||||
{
|
||||
var baseCellSize = _currentDesktopCellSize > 0
|
||||
? _currentDesktopCellSize * 1.10
|
||||
: 74;
|
||||
var densityBoost = Math.Max(widthCells, heightCells) >= 4 ? 8 : 0;
|
||||
return Math.Clamp(baseCellSize + densityBoost, PreviewRenderCellSizeMin, PreviewRenderCellSizeMax);
|
||||
}
|
||||
|
||||
private string BuildPreviewVisualSignature(ComponentPreviewKey key, double renderCellSize)
|
||||
{
|
||||
var appearance = _appearanceThemeService.GetCurrent();
|
||||
var renderScale = RenderScaling > 0 ? RenderScaling : 1d;
|
||||
return string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"{key}|Cell={renderCellSize:F2}|Scale={renderScale:F2}|Night={(appearance.IsNightMode ? 1 : 0)}|Corner={appearance.CornerRadiusStyle}|Accent={FormatSignatureColor(appearance.AccentColor)}");
|
||||
}
|
||||
|
||||
private ComponentPreviewKey CreateComponentTypePreviewKey(string componentId, int widthCells, int heightCells)
|
||||
{
|
||||
var span = NormalizeComponentCellSpan(componentId, (widthCells, heightCells));
|
||||
return ComponentPreviewKey.ForComponentType(componentId, span.WidthCells, span.HeightCells);
|
||||
}
|
||||
|
||||
private ComponentPreviewKey CreatePlacementPreviewKey(string componentId, string placementId, int widthCells, int heightCells)
|
||||
{
|
||||
var span = NormalizeComponentCellSpan(componentId, (widthCells, heightCells));
|
||||
return ComponentPreviewKey.ForPlacementInstance(componentId, placementId, span.WidthCells, span.HeightCells);
|
||||
}
|
||||
|
||||
private bool IsPlacementPresent(string? placementId)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(placementId) &&
|
||||
_desktopComponentPlacements.Any(candidate =>
|
||||
string.Equals(candidate.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private string BuildCurrentVisualSignature(ComponentPreviewKey key)
|
||||
{
|
||||
var renderCellSize = ResolvePreviewRenderCellSize(key.WidthCells, key.HeightCells);
|
||||
return BuildPreviewVisualSignature(key, renderCellSize);
|
||||
}
|
||||
|
||||
private bool TryGetReusablePreviewEntry(ComponentPreviewKey key, out ComponentPreviewImageEntry? entry)
|
||||
{
|
||||
if (!_componentPreviewImageService.TryGetEntry(key, out entry) ||
|
||||
entry is null ||
|
||||
entry.State != ComponentPreviewImageState.Ready ||
|
||||
entry.Bitmap is null)
|
||||
if (root is Panel panel)
|
||||
{
|
||||
entry = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedSignature = BuildCurrentVisualSignature(key);
|
||||
if (!string.Equals(entry.VisualSignature, expectedSignature, StringComparison.Ordinal))
|
||||
{
|
||||
entry = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private IImage? ResolvePreviewImageFromService(ComponentPreviewKey key)
|
||||
{
|
||||
if (!TryGetReusablePreviewEntry(key, out var entry) || entry is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.Bitmap;
|
||||
}
|
||||
|
||||
private ComponentPreviewImageEntry? ResolvePreviewEntry(ComponentPreviewKey key)
|
||||
{
|
||||
if (!_componentPreviewImageService.TryGetEntry(key, out var entry) || entry is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entry.State != ComponentPreviewImageState.Ready)
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
return TryGetReusablePreviewEntry(key, out var reusable) ? reusable : null;
|
||||
}
|
||||
|
||||
private IImage? ResolveComponentTypePreviewImage(string componentId, int widthCells, int heightCells)
|
||||
{
|
||||
var key = CreateComponentTypePreviewKey(componentId, widthCells, heightCells);
|
||||
return ResolvePreviewImageFromService(key);
|
||||
}
|
||||
|
||||
private IImage? ResolveDesktopEditPreviewImage(string componentId, string? placementId, int widthCells, int heightCells)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(placementId))
|
||||
{
|
||||
var placementKey = CreatePlacementPreviewKey(componentId, placementId, widthCells, heightCells);
|
||||
var placementImage = ResolvePreviewImageFromService(placementKey);
|
||||
if (placementImage is not null)
|
||||
foreach (var child in panel.Children.OfType<Control>())
|
||||
{
|
||||
return placementImage;
|
||||
foreach (var descendant in EnumerateControls(child))
|
||||
{
|
||||
yield return descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var componentTypeKey = CreateComponentTypePreviewKey(componentId, widthCells, heightCells);
|
||||
return ResolvePreviewImageFromService(componentTypeKey);
|
||||
}
|
||||
|
||||
private (int WidthCells, int HeightCells) ResolveOverlayPreviewSpan(
|
||||
string componentId,
|
||||
string? placementId,
|
||||
int? widthCells,
|
||||
int? heightCells)
|
||||
{
|
||||
if (widthCells is > 0 && heightCells is > 0)
|
||||
if (root is ContentControl { Content: Control content })
|
||||
{
|
||||
return NormalizeComponentCellSpan(componentId, (widthCells.Value, heightCells.Value));
|
||||
foreach (var descendant in EnumerateControls(content))
|
||||
{
|
||||
yield return descendant;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(placementId) &&
|
||||
TryGetDesktopPlacementById(placementId, out var placement))
|
||||
if (root is Decorator { Child: Control decoratorChild })
|
||||
{
|
||||
return NormalizeComponentCellSpan(componentId, (placement.WidthCells, placement.HeightCells));
|
||||
foreach (var descendant in EnumerateControls(decoratorChild))
|
||||
{
|
||||
yield return descendant;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_desktopEditSession.ComponentId) &&
|
||||
string.Equals(_desktopEditSession.ComponentId, componentId, StringComparison.OrdinalIgnoreCase) &&
|
||||
_desktopEditSession.WidthCells > 0 &&
|
||||
_desktopEditSession.HeightCells > 0)
|
||||
{
|
||||
return NormalizeComponentCellSpan(componentId, (_desktopEditSession.WidthCells, _desktopEditSession.HeightCells));
|
||||
}
|
||||
|
||||
if (_componentRuntimeRegistry.TryGetDescriptor(componentId, out var descriptor))
|
||||
{
|
||||
return NormalizeComponentCellSpan(
|
||||
componentId,
|
||||
(descriptor.Definition.MinWidthCells, descriptor.Definition.MinHeightCells));
|
||||
}
|
||||
|
||||
return (1, 1);
|
||||
}
|
||||
|
||||
private void ApplyDesktopEditOverlayPreviewImage(
|
||||
@@ -432,9 +130,12 @@ public partial class MainWindow
|
||||
int? widthCells = null,
|
||||
int? heightCells = null)
|
||||
{
|
||||
var span = ResolveOverlayPreviewSpan(componentId, placementId, widthCells, heightCells);
|
||||
_ = componentId;
|
||||
_ = placementId;
|
||||
_ = widthCells;
|
||||
_ = heightCells;
|
||||
EnsureDesktopEditOverlayPresenter();
|
||||
_desktopEditOverlayPresenter?.SetPreviewImage(ResolveDesktopEditPreviewImage(componentId, placementId, span.WidthCells, span.HeightCells));
|
||||
_desktopEditOverlayPresenter?.SetPreviewImage(null);
|
||||
}
|
||||
|
||||
private void PrimeDesktopEditPreviewImage(
|
||||
@@ -444,164 +145,28 @@ public partial class MainWindow
|
||||
int widthCells,
|
||||
int heightCells)
|
||||
{
|
||||
_ = componentId;
|
||||
_ = placementId;
|
||||
_ = pageIndex;
|
||||
var normalized = NormalizeComponentCellSpan(componentId, (widthCells, heightCells));
|
||||
_ = EnsureComponentTypePreviewImageAsync(componentId, normalized.WidthCells, normalized.HeightCells);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(placementId) &&
|
||||
TryGetDesktopPlacementById(placementId, out var placement))
|
||||
{
|
||||
_ = RefreshPlacementPreviewImageAsync(placement, forceRefresh: false);
|
||||
}
|
||||
_ = widthCells;
|
||||
_ = heightCells;
|
||||
}
|
||||
|
||||
private void QueuePlacementPreviewRefresh(DesktopComponentPlacementSnapshot? placement)
|
||||
{
|
||||
_ = RefreshPlacementPreviewImageAsync(placement, forceRefresh: true);
|
||||
_ = placement;
|
||||
}
|
||||
|
||||
private void RemovePlacementPreviewImage(string? placementId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(placementId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_componentPreviewImageService.RemovePlacementPreviews(placementId);
|
||||
_ = placementId;
|
||||
}
|
||||
|
||||
private void RemovePlacementPreviewImages(IEnumerable<DesktopComponentPlacementSnapshot> placements)
|
||||
{
|
||||
foreach (var placementId in placements
|
||||
.Select(placement => placement.PlacementId)
|
||||
.Where(static id => !string.IsNullOrWhiteSpace(id))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
RemovePlacementPreviewImage(placementId);
|
||||
}
|
||||
_ = placements;
|
||||
}
|
||||
|
||||
private void RegisterComponentLibraryPreviewVisual(ComponentPreviewKey key, Image image, Control fallback)
|
||||
{
|
||||
if (!_componentLibraryPreviewVisualTargets.TryGetValue(key, out var visuals))
|
||||
{
|
||||
visuals = [];
|
||||
_componentLibraryPreviewVisualTargets[key] = visuals;
|
||||
}
|
||||
|
||||
visuals.Add(new ComponentLibraryPreviewVisualTarget(image, fallback));
|
||||
}
|
||||
|
||||
private void ClearComponentLibraryPreviewVisualTargets()
|
||||
{
|
||||
_componentLibraryPreviewVisualTargets.Clear();
|
||||
}
|
||||
|
||||
private void ApplyPreviewEntryToEmbeddedVisuals(ComponentPreviewKey key)
|
||||
{
|
||||
if (!_componentLibraryPreviewVisualTargets.TryGetValue(key, out var targets))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var previewImage = ResolvePreviewImageFromService(key);
|
||||
foreach (var target in targets)
|
||||
{
|
||||
target.Image.Source = previewImage;
|
||||
target.Image.IsVisible = previewImage is not null;
|
||||
target.Fallback.IsVisible = previewImage is null;
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyPreviewEntryUpdated(ComponentPreviewImageEntry entry)
|
||||
{
|
||||
Dispatcher.UIThread.Post(
|
||||
() =>
|
||||
{
|
||||
ApplyPreviewEntryToEmbeddedVisuals(entry.Key);
|
||||
_detachedComponentLibraryWindow?.UpdatePreviewImage(entry);
|
||||
_fusedLibraryWindow?.UpdatePreviewImage(entry);
|
||||
|
||||
if (entry.Key.Kind == ComponentPreviewKeyKind.PlacementInstance)
|
||||
{
|
||||
RefreshDesktopEditOverlayPreviewIfActive(entry.Key.ComponentTypeId, entry.Key.PlacementId);
|
||||
}
|
||||
else
|
||||
{
|
||||
RefreshDesktopEditOverlayPreviewIfActive(entry.Key.ComponentTypeId, placementId: null);
|
||||
}
|
||||
},
|
||||
DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private static void DisposeImageIfNeeded(IImage? image)
|
||||
{
|
||||
if (image is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatSignatureColor(Color color)
|
||||
{
|
||||
return string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}");
|
||||
}
|
||||
|
||||
private void RefreshDesktopEditOverlayPreviewIfActive(string componentId, string? placementId)
|
||||
{
|
||||
if (_desktopEditOverlayPresenter is null ||
|
||||
(!_desktopEditSession.IsActive && !_isDesktopEditCommitPending) ||
|
||||
string.IsNullOrWhiteSpace(_desktopEditSession.ComponentId) ||
|
||||
!string.Equals(_desktopEditSession.ComponentId, componentId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(placementId) &&
|
||||
!string.Equals(_desktopEditSession.PlacementId, placementId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyDesktopEditOverlayPreviewImage(
|
||||
_desktopEditSession.ComponentId,
|
||||
_desktopEditSession.PlacementId,
|
||||
_desktopEditSession.WidthCells,
|
||||
_desktopEditSession.HeightCells);
|
||||
}
|
||||
|
||||
private ComponentPreviewKey ResolveDetachedLibraryPreviewKey(ComponentLibraryComponentEntry entry)
|
||||
{
|
||||
return CreateComponentTypePreviewKey(entry.ComponentId, entry.MinWidthCells, entry.MinHeightCells);
|
||||
}
|
||||
|
||||
private ComponentPreviewImageEntry? ResolveDetachedLibraryPreviewEntry(ComponentPreviewKey key)
|
||||
{
|
||||
return ResolvePreviewEntry(key);
|
||||
}
|
||||
|
||||
private void RequestDetachedLibraryPreviewWarm(ComponentPreviewKey key)
|
||||
{
|
||||
_ = QueuePreviewGenerationAsync(
|
||||
key,
|
||||
pageIndex: null,
|
||||
action: "DetachedLibraryWarm",
|
||||
forceRefresh: false);
|
||||
}
|
||||
|
||||
private void RequestDetachedLibraryPreviewRender(ComponentPreviewKey key)
|
||||
{
|
||||
_ = QueuePreviewGenerationAsync(
|
||||
key,
|
||||
pageIndex: null,
|
||||
action: "DetachedLibraryRender",
|
||||
forceRefresh: false);
|
||||
}
|
||||
|
||||
// FusedDesktop 支持
|
||||
|
||||
public void RegisterFusedLibraryWindow(FusedDesktopComponentLibraryWindow window)
|
||||
{
|
||||
_fusedLibraryWindow = window;
|
||||
@@ -614,15 +179,4 @@ public partial class MainWindow
|
||||
_fusedLibraryWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
public ComponentPreviewImageEntry? GetPreviewEntry(ComponentPreviewKey key)
|
||||
{
|
||||
return ResolvePreviewEntry(key);
|
||||
}
|
||||
|
||||
public void RequestDetachedLibraryPreview(ComponentPreviewKey key)
|
||||
{
|
||||
RequestDetachedLibraryPreviewWarm(key);
|
||||
RequestDetachedLibraryPreviewRender(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ using SymbolIcon = FluentIcons.Avalonia.SymbolIcon;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class MainWindow
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly List<DesktopComponentPlacementSnapshot> _desktopComponentPlacements = [];
|
||||
private readonly Dictionary<int, Grid> _desktopPageComponentGrids = new();
|
||||
@@ -390,12 +390,10 @@ public partial class MainWindow
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// Windows: 使用 SlideToShutDown 滑动关机界面
|
||||
_powerService.ShowNativePowerUI(PowerAction.Shutdown);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Linux: 二次确认对话框
|
||||
await ShowPowerConfirmDialogAsync(L("power.shutdown_confirm_title", "Shutdown"),
|
||||
L("power.shutdown_confirm_message", "Are you sure you want to shut down this computer?"),
|
||||
() => _powerService.ShutdownAsync());
|
||||
@@ -408,8 +406,6 @@ public partial class MainWindow
|
||||
_ = e;
|
||||
ClosePopupIfOpen();
|
||||
|
||||
// 所有平台:统一使用二次确认对话框
|
||||
// Note: SlideToShutDown.exe 只支持关机,不支持重启
|
||||
await ShowPowerConfirmDialogAsync(L("power.restart_confirm_title", "Restart"),
|
||||
L("power.restart_confirm_message", "Are you sure you want to restart this computer?"),
|
||||
() => _powerService.RestartAsync());
|
||||
@@ -449,7 +445,7 @@ public partial class MainWindow
|
||||
{
|
||||
try
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
var dialog = new FAContentDialog
|
||||
{
|
||||
Title = title,
|
||||
Content = message,
|
||||
@@ -458,7 +454,7 @@ public partial class MainWindow
|
||||
};
|
||||
|
||||
var result = await dialog.ShowAsync(this);
|
||||
if (result == ContentDialogResult.Primary)
|
||||
if (result == FAContentDialogResult.Primary)
|
||||
{
|
||||
await action();
|
||||
}
|
||||
@@ -744,49 +740,42 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测状态栏组件是否会发生碰撞
|
||||
/// </summary>
|
||||
/// 濠碘槅鍋€閸嬫挻绻涢弶鎴剳濠殿喗鎮傞獮鈧ù锝呮贡閸╁绱撴担绋款仹婵炲棎鍨藉浼搭敍濮橆厼鍓ㄦ繛鏉戝悑閼规崘銇愰崒鐐村仺闁绘柧璀﹀楣冩煙? /// </summary>
|
||||
private bool WouldComponentsCollide()
|
||||
{
|
||||
if (TopStatusBarHost is null)
|
||||
return false;
|
||||
|
||||
// 获取各区域当前占用的宽度
|
||||
var leftWidth = GetLeftPanelOccupiedWidth();
|
||||
var centerWidth = GetCenterPanelOccupiedWidth();
|
||||
var rightWidth = GetRightPanelOccupiedWidth();
|
||||
|
||||
// 获取状态栏总宽度
|
||||
var totalWidth = TopStatusBarHost.Bounds.Width;
|
||||
if (totalWidth <= 0)
|
||||
return false;
|
||||
|
||||
// 计算中间区域的实际位置
|
||||
// 左列是 *, 中列是 Auto, 右列是 *
|
||||
// 中间区域居中显示
|
||||
// 闁荤姳绶ょ槐鏇㈡偩缂佹鈻旀い鎾卞灪閿涚喖鏌涢弽褎鎯堥柣鎾寸懇閹啴宕熼銈嗘緰闂傚倸瀚幊宥囩礊閸涱垳纾? // 閻庡綊娼荤粻鎴﹀垂椤忓牆鍙?*, 婵炴垶鎼╅崢濂稿垂椤忓牆鍙?Auto, 闂佸憡鐟ラ崯鍧楀垂椤忓牆鍙?*
|
||||
// 婵炴垶鎼╅崣鍐ㄎ涢崸妤€绀岄柛婵嗗閸樼敻鎮橀悙鍙夊櫢闁煎灚鍨垮浼村礈瑜嬫禒?
|
||||
var centerLeft = (totalWidth - centerWidth) / 2;
|
||||
var centerRight = centerLeft + centerWidth;
|
||||
|
||||
// 安全间距(像素)
|
||||
// 闁诲海鎳撻ˇ顖炲矗韫囨稒鈷掔痪鎯ь儑閻涒晠鏌ㄥ☉妯煎闁稿孩姘ㄥΣ鎰版偑閸涱垳顦?
|
||||
const double safetyMargin = 20;
|
||||
|
||||
// 检测左侧组件是否会与中间区域碰撞
|
||||
// 左侧组件右边界 = leftWidth
|
||||
// 中间区域左边界 = centerLeft
|
||||
// 濠碘槅鍋€閸嬫挻绻涢弶鎴剰濞戞柨绻戠粭鐔活槾缂侇喖绉电粋鎺楁嚋閸倣锕傛煕濮樺墽绱扮紒杈╁缁嬪鎯斿┑濠傚箑闂傚倸鍊瑰娆戜焊椤栫偛鏄ラ柣鏂捐濞奸箖鏌? // 閻庡綊娼荤紓姘跺疾閸撲胶纾奸柛鏇ㄤ簼椤愪粙鏌涘▎蹇曟瀮缂佹梻鍠栭幃?= leftWidth
|
||||
// 婵炴垶鎼╅崣鍐ㄎ涢崸妤€绀岄柛婵嗗閸樼數鈧綊娼荤粻鎺旂博閻斿吋鍋?= centerLeft
|
||||
if (leftWidth + safetyMargin > centerLeft)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检测右侧组件是否会与中间区域碰撞
|
||||
// 右侧组件左边界 = totalWidth - rightWidth
|
||||
// 中间区域右边界 = centerRight
|
||||
// 濠碘槅鍋€閸嬫挻绻涢弶鎴剰鐟滄澘鎲$粭鐔活槾缂侇喖绉电粋鎺楁嚋閸倣锕傛煕濮樺墽绱扮紒杈╁缁嬪鎯斿┑濠傚箑闂傚倸鍊瑰娆戜焊椤栫偛鏄ラ柣鏂捐濞奸箖鏌? // 闂佸憡鐟ラ崢鏍疾閸撲胶纾奸柛鏇ㄤ簼椤愮晫鈧綊娼荤粻鎺旂博閻斿吋鍋?= totalWidth - rightWidth
|
||||
// 婵炴垶鎼╅崣鍐ㄎ涢崸妤€绀岄柛婵嗗閸樼敻鏌涘▎蹇曟瀮缂佹梻鍠栭幃?= centerRight
|
||||
if (totalWidth - rightWidth - safetyMargin < centerRight)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检测中间区域是否会与左右两侧碰撞(中间区域过宽)
|
||||
if (centerLeft < leftWidth + safetyMargin ||
|
||||
centerRight > totalWidth - rightWidth - safetyMargin)
|
||||
{
|
||||
@@ -797,8 +786,7 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取左侧面板占用的宽度(包括间距)
|
||||
/// </summary>
|
||||
/// 闂佸吋鍎抽崲鑼躲亹閸パ屽晠闁挎梹瀵у▍鐘绘⒒閸稑鐏繝銏★耿瀹曪繝鎮╅崹顐f闂佹眹鍔岀€氼剟顢欓弮鈧幆鏃堟晜閼测晝顦╅梺鍛婄墪閹冲繒鈧凹鍙冨鑽ゅ鐎n剛宕洪梺? /// </summary>
|
||||
private double GetLeftPanelOccupiedWidth()
|
||||
{
|
||||
if (TopStatusLeftPanel is null)
|
||||
@@ -817,8 +805,7 @@ public partial class MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
// 添加间距
|
||||
if (visibleCount > 1)
|
||||
// 濠电儑缍€椤曆勬叏閻愮儤鈷掔痪鎯ь儑閻? if (visibleCount > 1)
|
||||
{
|
||||
width += spacing * (visibleCount - 1);
|
||||
}
|
||||
@@ -827,8 +814,7 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取中间面板占用的宽度(包括间距)
|
||||
/// </summary>
|
||||
/// 闂佸吋鍎抽崲鑼躲亹閸ャ劎鈻旀い鎾卞灪閿涚喖姊婚崼娑樼仾婵犮垺锕㈠畷锟犳偐閸偅娈㈤梺姹囧妼鐎氼剟顢欓弮鈧幆鏃堟晜閼测晝顦╅梺鍛婄墪閹冲繒鈧凹鍙冨鑽ゅ鐎n剛宕洪梺? /// </summary>
|
||||
private double GetCenterPanelOccupiedWidth()
|
||||
{
|
||||
if (TopStatusCenterPanel is null)
|
||||
@@ -847,8 +833,7 @@ public partial class MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
// 添加间距
|
||||
if (visibleCount > 1)
|
||||
// 濠电儑缍€椤曆勬叏閻愮儤鈷掔痪鎯ь儑閻? if (visibleCount > 1)
|
||||
{
|
||||
width += spacing * (visibleCount - 1);
|
||||
}
|
||||
@@ -857,8 +842,7 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取右侧面板占用的宽度(包括间距)
|
||||
/// </summary>
|
||||
/// 闂佸吋鍎抽崲鑼躲亹閸ヮ剙鐭楅柛蹇撴噺濞呯娀姊婚崼娑樼仾婵犮垺锕㈠畷锟犳偐閸偅娈㈤梺姹囧妼鐎氼剟顢欓弮鈧幆鏃堟晜閼测晝顦╅梺鍛婄墪閹冲繒鈧凹鍙冨鑽ゅ鐎n剛宕洪梺? /// </summary>
|
||||
private double GetRightPanelOccupiedWidth()
|
||||
{
|
||||
if (TopStatusRightPanel is null)
|
||||
@@ -877,8 +861,7 @@ public partial class MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
// 添加间距
|
||||
if (visibleCount > 1)
|
||||
// 濠电儑缍€椤曆勬叏閻愮儤鈷掔痪鎯ь儑閻? if (visibleCount > 1)
|
||||
{
|
||||
width += spacing * (visibleCount - 1);
|
||||
}
|
||||
@@ -887,25 +870,19 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以在指定位置添加组件
|
||||
/// </summary>
|
||||
/// 濠碘槅鍋€閸嬫捇鏌$仦璇插姕婵″弶鎮傚畷銉╂晝閳ь剝銇愰崣澶岊浄闁靛鍎查煬顒勬煙缁嬫寧鎼愰柣锝囧亾閹峰懎顓奸崶鈺傜€┑鐑囩秬椤曆勬叏閻愮數纾奸柛鏇ㄤ簼椤? /// </summary>
|
||||
private bool CanAddComponentAtPosition(string position)
|
||||
{
|
||||
// 先临时显示组件以计算宽度
|
||||
var wouldCollide = WouldComponentsCollide();
|
||||
if (!wouldCollide)
|
||||
return true;
|
||||
|
||||
// 如果会发生碰撞,检查是否是因为目标位置导致的
|
||||
// 获取当前各区域宽度
|
||||
var leftWidth = GetLeftPanelOccupiedWidth();
|
||||
var centerWidth = GetCenterPanelOccupiedWidth();
|
||||
var rightWidth = GetRightPanelOccupiedWidth();
|
||||
|
||||
// 估算新组件的宽度(基于当前单元格大小)
|
||||
var estimatedNewComponentWidth = _currentDesktopCellSize > 0 ? _currentDesktopCellSize * 2 : 120;
|
||||
|
||||
// 根据目标位置检查添加后是否会碰撞
|
||||
return position switch
|
||||
{
|
||||
"Left" => CanAddToLeft(leftWidth, centerWidth, rightWidth, estimatedNewComponentWidth),
|
||||
@@ -924,7 +901,7 @@ public partial class MainWindow
|
||||
if (totalWidth <= 0)
|
||||
return true;
|
||||
|
||||
var newLeftWidth = leftWidth + newWidth + TopStatusLeftPanel?.Spacing ?? 6;
|
||||
var newLeftWidth = leftWidth + newWidth + (TopStatusLeftPanel?.Spacing ?? 6);
|
||||
var centerLeft = (totalWidth - centerWidth) / 2;
|
||||
|
||||
const double safetyMargin = 20;
|
||||
@@ -940,7 +917,7 @@ public partial class MainWindow
|
||||
if (totalWidth <= 0)
|
||||
return true;
|
||||
|
||||
var newCenterWidth = centerWidth + newWidth + TopStatusCenterPanel?.Spacing ?? 6;
|
||||
var newCenterWidth = centerWidth + newWidth + (TopStatusCenterPanel?.Spacing ?? 6);
|
||||
var centerLeft = (totalWidth - newCenterWidth) / 2;
|
||||
var centerRight = centerLeft + newCenterWidth;
|
||||
|
||||
@@ -958,7 +935,7 @@ public partial class MainWindow
|
||||
if (totalWidth <= 0)
|
||||
return true;
|
||||
|
||||
var newRightWidth = rightWidth + newWidth + TopStatusRightPanel?.Spacing ?? 6;
|
||||
var newRightWidth = rightWidth + newWidth + (TopStatusRightPanel?.Spacing ?? 6);
|
||||
var centerRight = (totalWidth + centerWidth) / 2;
|
||||
|
||||
const double safetyMargin = 20;
|
||||
@@ -970,7 +947,6 @@ public partial class MainWindow
|
||||
var showClock = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock);
|
||||
var hasVisibleTopStatusComponent = false;
|
||||
|
||||
// 先隐藏所有时钟控件
|
||||
if (ClockWidgetLeft is not null)
|
||||
ClockWidgetLeft.IsVisible = false;
|
||||
if (ClockWidgetCenter is not null)
|
||||
@@ -978,7 +954,6 @@ public partial class MainWindow
|
||||
if (ClockWidgetRight is not null)
|
||||
ClockWidgetRight.IsVisible = false;
|
||||
|
||||
// 先隐藏所有文字胶囊控件
|
||||
if (TextCapsuleWidgetLeft is not null)
|
||||
TextCapsuleWidgetLeft.IsVisible = false;
|
||||
if (TextCapsuleWidgetCenter is not null)
|
||||
@@ -986,7 +961,6 @@ public partial class MainWindow
|
||||
if (TextCapsuleWidgetRight is not null)
|
||||
TextCapsuleWidgetRight.IsVisible = false;
|
||||
|
||||
// 先隐藏所有网速控件
|
||||
if (NetworkSpeedWidgetLeft is not null)
|
||||
NetworkSpeedWidgetLeft.IsVisible = false;
|
||||
if (NetworkSpeedWidgetCenter is not null)
|
||||
@@ -994,7 +968,6 @@ public partial class MainWindow
|
||||
if (NetworkSpeedWidgetRight is not null)
|
||||
NetworkSpeedWidgetRight.IsVisible = false;
|
||||
|
||||
// 根据位置设置显示对应的时钟控件(带碰撞检测)
|
||||
if (showClock)
|
||||
{
|
||||
var targetPosition = _clockPosition;
|
||||
@@ -1019,7 +992,6 @@ public partial class MainWindow
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果目标位置无法添加,尝试其他位置
|
||||
var alternativePosition = FindAlternativePosition(targetPosition);
|
||||
if (alternativePosition is not null)
|
||||
{
|
||||
@@ -1041,8 +1013,7 @@ public partial class MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
// 根据位置设置显示对应的文字胶囊控件(带碰撞检测)
|
||||
if (_showTextCapsule)
|
||||
// 闂佸搫绉烽~澶婄暤娴h濯寸€广儱娲ㄩ弸鍌炴偣娴g鈷旈柣銈呮瀵即宕滆娴犳盯鎮楅悽鍨殌缂併劍鐓¢幆鍐礋椤掍胶鈧噣鎮楀☉娆樻畽闁稿繐鐭傚畷鑸电節閸愩劋绮繛瀵稿Ь椤旀劗妲愬▎鎴炴殰闁挎梻铏庡楣冩煙閸撗冧沪妞ゃ儱鎳庨湁閻庯絽澧庣粈? if (_showTextCapsule)
|
||||
{
|
||||
var targetPosition = _textCapsulePosition;
|
||||
var canAdd = CanAddComponentAtPosition(targetPosition);
|
||||
@@ -1066,7 +1037,6 @@ public partial class MainWindow
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果目标位置无法添加,尝试其他位置
|
||||
var alternativePosition = FindAlternativePosition(targetPosition);
|
||||
if (alternativePosition is not null)
|
||||
{
|
||||
@@ -1088,7 +1058,6 @@ public partial class MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
// 根据位置设置显示对应的网速控件(带碰撞检测)
|
||||
if (_showNetworkSpeed)
|
||||
{
|
||||
var targetPosition = _networkSpeedPosition;
|
||||
@@ -1113,7 +1082,6 @@ public partial class MainWindow
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果目标位置无法添加,尝试其他位置
|
||||
var alternativePosition = FindAlternativePosition(targetPosition);
|
||||
if (alternativePosition is not null)
|
||||
{
|
||||
@@ -1140,7 +1108,6 @@ public partial class MainWindow
|
||||
TopStatusBarHost.IsVisible = hasVisibleTopStatusComponent;
|
||||
}
|
||||
|
||||
// 延迟检查碰撞并调整
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await System.Threading.Tasks.Task.Delay(50);
|
||||
@@ -1149,22 +1116,18 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当组件发生碰撞时,自动调整位置
|
||||
/// </summary>
|
||||
/// 閻熸粎澧楅幐鍓у垝瀹ュ棛顩烽悹鍝勬惈缁叉椽鏌i姀銏犳灁妞ゎ偒鍋婇獮姗€鎮欑€涙﹩妲梺鎸庣☉閻線宕靛鍫濈闁靛鍔庡▓鍫曟煛娴h櫣绡€缂傚秴鎳愮槐? /// </summary>
|
||||
private void AdjustComponentsIfColliding()
|
||||
{
|
||||
if (!WouldComponentsCollide())
|
||||
return;
|
||||
|
||||
// 获取当前可见的组件
|
||||
var leftComponents = GetVisibleLeftComponents();
|
||||
var centerComponents = GetVisibleCenterComponents();
|
||||
var rightComponents = GetVisibleRightComponents();
|
||||
|
||||
// 优先保留时钟,调整文字胶囊位置
|
||||
if (TextCapsuleWidgetLeft?.IsVisible == true && WouldComponentsCollide())
|
||||
{
|
||||
// 尝试将左侧文字胶囊移到中间
|
||||
if (CanAddComponentAtPosition("Center"))
|
||||
{
|
||||
TextCapsuleWidgetLeft.IsVisible = false;
|
||||
@@ -1172,7 +1135,6 @@ public partial class MainWindow
|
||||
TextCapsuleWidgetCenter.SetTransparentBackground(_textCapsuleTransparentBackground);
|
||||
TextCapsuleWidgetCenter.SetText(_textCapsuleContent);
|
||||
}
|
||||
// 或者移到右侧
|
||||
else if (CanAddComponentAtPosition("Right"))
|
||||
{
|
||||
TextCapsuleWidgetLeft.IsVisible = false;
|
||||
@@ -1180,7 +1142,6 @@ public partial class MainWindow
|
||||
TextCapsuleWidgetRight.SetTransparentBackground(_textCapsuleTransparentBackground);
|
||||
TextCapsuleWidgetRight.SetText(_textCapsuleContent);
|
||||
}
|
||||
// 如果都无法添加,则隐藏文字胶囊
|
||||
else
|
||||
{
|
||||
TextCapsuleWidgetLeft.IsVisible = false;
|
||||
@@ -1189,7 +1150,6 @@ public partial class MainWindow
|
||||
|
||||
if (TextCapsuleWidgetRight?.IsVisible == true && WouldComponentsCollide())
|
||||
{
|
||||
// 尝试将右侧文字胶囊移到中间
|
||||
if (CanAddComponentAtPosition("Center"))
|
||||
{
|
||||
TextCapsuleWidgetRight.IsVisible = false;
|
||||
@@ -1197,7 +1157,6 @@ public partial class MainWindow
|
||||
TextCapsuleWidgetCenter.SetTransparentBackground(_textCapsuleTransparentBackground);
|
||||
TextCapsuleWidgetCenter.SetText(_textCapsuleContent);
|
||||
}
|
||||
// 或者移到左侧
|
||||
else if (CanAddComponentAtPosition("Left"))
|
||||
{
|
||||
TextCapsuleWidgetRight.IsVisible = false;
|
||||
@@ -1205,7 +1164,6 @@ public partial class MainWindow
|
||||
TextCapsuleWidgetLeft.SetTransparentBackground(_textCapsuleTransparentBackground);
|
||||
TextCapsuleWidgetLeft.SetText(_textCapsuleContent);
|
||||
}
|
||||
// 如果都无法添加,则隐藏文字胶囊
|
||||
else
|
||||
{
|
||||
TextCapsuleWidgetRight.IsVisible = false;
|
||||
@@ -1214,7 +1172,6 @@ public partial class MainWindow
|
||||
|
||||
if (TextCapsuleWidgetCenter?.IsVisible == true && WouldComponentsCollide())
|
||||
{
|
||||
// 尝试将中间文字胶囊移到左侧
|
||||
if (CanAddComponentAtPosition("Left"))
|
||||
{
|
||||
TextCapsuleWidgetCenter.IsVisible = false;
|
||||
@@ -1222,7 +1179,6 @@ public partial class MainWindow
|
||||
TextCapsuleWidgetLeft.SetTransparentBackground(_textCapsuleTransparentBackground);
|
||||
TextCapsuleWidgetLeft.SetText(_textCapsuleContent);
|
||||
}
|
||||
// 或者移到右侧
|
||||
else if (CanAddComponentAtPosition("Right"))
|
||||
{
|
||||
TextCapsuleWidgetCenter.IsVisible = false;
|
||||
@@ -1230,17 +1186,14 @@ public partial class MainWindow
|
||||
TextCapsuleWidgetRight.SetTransparentBackground(_textCapsuleTransparentBackground);
|
||||
TextCapsuleWidgetRight.SetText(_textCapsuleContent);
|
||||
}
|
||||
// 如果都无法添加,则隐藏文字胶囊
|
||||
else
|
||||
{
|
||||
TextCapsuleWidgetCenter.IsVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 调整网速组件位置(优先级:时钟 > 文字胶囊 > 网速)
|
||||
if (NetworkSpeedWidgetLeft?.IsVisible == true && WouldComponentsCollide())
|
||||
{
|
||||
// 尝试将左侧网速移到中间
|
||||
if (CanAddComponentAtPosition("Center"))
|
||||
{
|
||||
NetworkSpeedWidgetLeft.IsVisible = false;
|
||||
@@ -1248,7 +1201,6 @@ public partial class MainWindow
|
||||
NetworkSpeedWidgetCenter.SetTransparentBackground(_networkSpeedTransparentBackground);
|
||||
NetworkSpeedWidgetCenter.SetDisplayMode(_networkSpeedDisplayMode);
|
||||
}
|
||||
// 或者移到右侧
|
||||
else if (CanAddComponentAtPosition("Right"))
|
||||
{
|
||||
NetworkSpeedWidgetLeft.IsVisible = false;
|
||||
@@ -1256,7 +1208,6 @@ public partial class MainWindow
|
||||
NetworkSpeedWidgetRight.SetTransparentBackground(_networkSpeedTransparentBackground);
|
||||
NetworkSpeedWidgetRight.SetDisplayMode(_networkSpeedDisplayMode);
|
||||
}
|
||||
// 如果都无法添加,则隐藏网速
|
||||
else
|
||||
{
|
||||
NetworkSpeedWidgetLeft.IsVisible = false;
|
||||
@@ -1265,7 +1216,6 @@ public partial class MainWindow
|
||||
|
||||
if (NetworkSpeedWidgetRight?.IsVisible == true && WouldComponentsCollide())
|
||||
{
|
||||
// 尝试将右侧网速移到中间
|
||||
if (CanAddComponentAtPosition("Center"))
|
||||
{
|
||||
NetworkSpeedWidgetRight.IsVisible = false;
|
||||
@@ -1273,7 +1223,6 @@ public partial class MainWindow
|
||||
NetworkSpeedWidgetCenter.SetTransparentBackground(_networkSpeedTransparentBackground);
|
||||
NetworkSpeedWidgetCenter.SetDisplayMode(_networkSpeedDisplayMode);
|
||||
}
|
||||
// 或者移到左侧
|
||||
else if (CanAddComponentAtPosition("Left"))
|
||||
{
|
||||
NetworkSpeedWidgetRight.IsVisible = false;
|
||||
@@ -1281,7 +1230,6 @@ public partial class MainWindow
|
||||
NetworkSpeedWidgetLeft.SetTransparentBackground(_networkSpeedTransparentBackground);
|
||||
NetworkSpeedWidgetLeft.SetDisplayMode(_networkSpeedDisplayMode);
|
||||
}
|
||||
// 如果都无法添加,则隐藏网速
|
||||
else
|
||||
{
|
||||
NetworkSpeedWidgetRight.IsVisible = false;
|
||||
@@ -1290,7 +1238,6 @@ public partial class MainWindow
|
||||
|
||||
if (NetworkSpeedWidgetCenter?.IsVisible == true && WouldComponentsCollide())
|
||||
{
|
||||
// 尝试将中间网速移到左侧
|
||||
if (CanAddComponentAtPosition("Left"))
|
||||
{
|
||||
NetworkSpeedWidgetCenter.IsVisible = false;
|
||||
@@ -1298,7 +1245,6 @@ public partial class MainWindow
|
||||
NetworkSpeedWidgetLeft.SetTransparentBackground(_networkSpeedTransparentBackground);
|
||||
NetworkSpeedWidgetLeft.SetDisplayMode(_networkSpeedDisplayMode);
|
||||
}
|
||||
// 或者移到右侧
|
||||
else if (CanAddComponentAtPosition("Right"))
|
||||
{
|
||||
NetworkSpeedWidgetCenter.IsVisible = false;
|
||||
@@ -1306,7 +1252,6 @@ public partial class MainWindow
|
||||
NetworkSpeedWidgetRight.SetTransparentBackground(_networkSpeedTransparentBackground);
|
||||
NetworkSpeedWidgetRight.SetDisplayMode(_networkSpeedDisplayMode);
|
||||
}
|
||||
// 如果都无法添加,则隐藏网速
|
||||
else
|
||||
{
|
||||
NetworkSpeedWidgetCenter.IsVisible = false;
|
||||
@@ -1315,11 +1260,10 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找可用的替代位置
|
||||
/// </summary>
|
||||
/// 闂佸搫琚崕鍙夌珶濮椻偓瀹曪綁顢涘鍕闂佹眹鍔岀€氼厼霉濞戞瑧顩烽柨婵嗗缁夊绱? /// </summary>
|
||||
private string? FindAlternativePosition(string originalPosition)
|
||||
{
|
||||
// 尝试所有可能的位置
|
||||
// 闁诲繐绻戠换鍡涙儊椤栫偛绠ラ柍褜鍓熷鍨緞婵犲倽顔夐梺鐓庣-閺咁偄鈻撻幋鐐村鐎广儱娲ㄩ弸?
|
||||
var positions = new[] { "Left", "Center", "Right" };
|
||||
foreach (var position in positions)
|
||||
{
|
||||
@@ -1332,8 +1276,7 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取左侧可见组件列表
|
||||
/// </summary>
|
||||
/// 闂佸吋鍎抽崲鑼躲亹閸パ屽晠闁挎梹瀵у▍鐘绘煕濞嗘ê鐏ユい顐㈡缁辨帡宕熼鍜佸仺闂佸憡甯楅〃澶愬Υ? /// </summary>
|
||||
private List<Control> GetVisibleLeftComponents()
|
||||
{
|
||||
var result = new List<Control>();
|
||||
@@ -1348,8 +1291,7 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取中间可见组件列表
|
||||
/// </summary>
|
||||
/// 闂佸吋鍎抽崲鑼躲亹閸ャ劎鈻旀い鎾卞灪閿涚喖鏌涘▎妯虹仴妞ゎ偄妫涚槐鎺楀礋椤忓拋鍋ㄩ梺鍛婂笚椤ㄥ濡? /// </summary>
|
||||
private List<Control> GetVisibleCenterComponents()
|
||||
{
|
||||
var result = new List<Control>();
|
||||
@@ -1364,8 +1306,7 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取右侧可见组件列表
|
||||
/// </summary>
|
||||
/// 闂佸吋鍎抽崲鑼躲亹閸ヮ剙鐭楅柛蹇撴噺濞呯娀鏌涘▎妯虹仴妞ゎ偄妫涚槐鎺楀礋椤忓拋鍋ㄩ梺鍛婂笚椤ㄥ濡? /// </summary>
|
||||
private List<Control> GetVisibleRightComponents()
|
||||
{
|
||||
var result = new List<Control>();
|
||||
@@ -1539,13 +1480,11 @@ public partial class MainWindow
|
||||
_weatherDataService,
|
||||
_recommendationInfoService,
|
||||
_calculatorDataService,
|
||||
_settingsFacade);
|
||||
_settingsFacade,
|
||||
PlacementId: null,
|
||||
RenderMode: DesktopComponentRenderMode.LibraryPreview);
|
||||
},
|
||||
L,
|
||||
previewKeyResolver: ResolveDetachedLibraryPreviewKey,
|
||||
previewEntryResolver: ResolveDetachedLibraryPreviewEntry,
|
||||
warmPreviewRequested: RequestDetachedLibraryPreviewWarm,
|
||||
renderPreviewRequested: RequestDetachedLibraryPreviewRender);
|
||||
L);
|
||||
window.AddComponentRequested += OnDetachedComponentLibraryAddComponentRequested;
|
||||
window.Closed += OnDetachedComponentLibraryClosed;
|
||||
return window;
|
||||
@@ -1833,17 +1772,17 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new ContentDialog
|
||||
var dialog = new FAContentDialog
|
||||
{
|
||||
Title = L("desktop.delete_page_confirm.title", "确认删除页面"),
|
||||
Content = L("desktop.delete_page_confirm.message", "确定要删除当前页面吗?\n\n此操作将删除当前页面上的所有组件,且无法撤销。"),
|
||||
PrimaryButtonText = L("desktop.delete_page_confirm.close", "取消"),
|
||||
SecondaryButtonText = L("desktop.delete_page_confirm.primary", "删除"),
|
||||
DefaultButton = ContentDialogButton.Primary
|
||||
Title = L("desktop.delete_page_confirm.title", "Delete desktop page"),
|
||||
Content = L("desktop.delete_page_confirm.message", "This will permanently remove the current desktop page and all widgets placed on it.\n\nThis action cannot be undone."),
|
||||
PrimaryButtonText = L("desktop.delete_page_confirm.close", "Cancel"),
|
||||
SecondaryButtonText = L("desktop.delete_page_confirm.primary", "Delete"),
|
||||
DefaultButton = FAContentDialogButton.Primary
|
||||
};
|
||||
|
||||
var result = await dialog.ShowAsync(this);
|
||||
if (result == ContentDialogResult.Secondary)
|
||||
if (result == FAContentDialogResult.Secondary)
|
||||
{
|
||||
DeleteCurrentDesktopPage();
|
||||
}
|
||||
@@ -3679,7 +3618,6 @@ public partial class MainWindow
|
||||
var category = _componentLibraryCategories[_componentLibraryCategoryIndex];
|
||||
_componentLibraryActiveCategoryId = category.Id;
|
||||
_componentLibraryComponentIndex = 0;
|
||||
_ = WarmComponentLibraryCategoryPreviewsAsync(category);
|
||||
BuildComponentLibraryComponentPages(category);
|
||||
ShowComponentLibraryComponentsView();
|
||||
}
|
||||
@@ -3697,10 +3635,10 @@ public partial class MainWindow
|
||||
var componentCount = _componentLibraryActiveComponents.Count;
|
||||
|
||||
ClearTimeZoneServiceBindings(ComponentLibraryComponentPagesContainer.Children.OfType<Control>().ToList());
|
||||
DisposeStaticComponentLibraryPreviews(ComponentLibraryComponentPagesContainer.Children.OfType<Control>());
|
||||
ComponentLibraryComponentPagesContainer.Children.Clear();
|
||||
ComponentLibraryComponentPagesContainer.RowDefinitions.Clear();
|
||||
ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear();
|
||||
ClearComponentLibraryPreviewVisualTargets();
|
||||
if (componentCount == 0)
|
||||
{
|
||||
_componentLibraryComponentIndex = 0;
|
||||
@@ -3774,51 +3712,22 @@ public partial class MainWindow
|
||||
|
||||
var previewWidth = previewSpan.WidthCells * previewCellSize;
|
||||
var previewHeight = previewSpan.HeightCells * previewCellSize;
|
||||
var previewKey = CreateComponentTypePreviewKey(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells);
|
||||
var cachedPreviewImage = ResolveComponentTypePreviewImage(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells);
|
||||
var previewControl = CreateStaticComponentLibraryPreview(
|
||||
component.ComponentId,
|
||||
previewCellSize,
|
||||
previewWidth,
|
||||
previewHeight);
|
||||
|
||||
var previewImage = new Image
|
||||
var previewSurface = new Border
|
||||
{
|
||||
Width = previewWidth,
|
||||
Height = previewHeight,
|
||||
Stretch = Stretch.Uniform,
|
||||
Source = cachedPreviewImage,
|
||||
IsVisible = cachedPreviewImage is not null,
|
||||
Background = Brushes.Transparent,
|
||||
ClipToBounds = false,
|
||||
Child = previewControl,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
|
||||
var previewFallback = new Border
|
||||
{
|
||||
Width = previewWidth,
|
||||
Height = previewHeight,
|
||||
Background = GetThemeBrush("AdaptiveCardBackgroundBrush"),
|
||||
BorderBrush = GetThemeBrush("AdaptiveButtonBorderBrush"),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(Math.Clamp(Math.Min(previewWidth, previewHeight) * 0.18, 12, 28)),
|
||||
IsVisible = cachedPreviewImage is null,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = L("component_library.preview_loading", "Preparing preview"),
|
||||
FontSize = 11,
|
||||
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush"),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
}
|
||||
};
|
||||
RegisterComponentLibraryPreviewVisual(previewKey, previewImage, previewFallback);
|
||||
|
||||
var previewSurface = new Grid
|
||||
{
|
||||
Width = previewWidth,
|
||||
Height = previewHeight,
|
||||
IsHitTestVisible = false,
|
||||
Children =
|
||||
{
|
||||
previewImage,
|
||||
previewFallback
|
||||
}
|
||||
};
|
||||
|
||||
var previewBorder = new Border
|
||||
{
|
||||
Width = previewWidth,
|
||||
@@ -3866,15 +3775,6 @@ public partial class MainWindow
|
||||
Grid.SetRow(page, 0);
|
||||
Grid.SetColumn(page, i);
|
||||
ComponentLibraryComponentPagesContainer.Children.Add(page);
|
||||
|
||||
if (cachedPreviewImage is null)
|
||||
{
|
||||
_ = EnsureComponentTypePreviewImageAsync(component.ComponentId, previewSpan.WidthCells, previewSpan.HeightCells);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyPreviewEntryToEmbeddedVisuals(previewKey);
|
||||
}
|
||||
}
|
||||
|
||||
_componentLibraryComponentHostTransform = ComponentLibraryComponentPagesHost.RenderTransform as TranslateTransform;
|
||||
@@ -3896,10 +3796,10 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
ClearTimeZoneServiceBindings(ComponentLibraryComponentPagesContainer.Children.OfType<Control>().ToList());
|
||||
DisposeStaticComponentLibraryPreviews(ComponentLibraryComponentPagesContainer.Children.OfType<Control>());
|
||||
ComponentLibraryComponentPagesContainer.Children.Clear();
|
||||
ComponentLibraryComponentPagesContainer.RowDefinitions.Clear();
|
||||
ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear();
|
||||
ClearComponentLibraryPreviewVisualTargets();
|
||||
}
|
||||
|
||||
private string GetLocalizedComponentDisplayName(ComponentLibraryComponentEntry component)
|
||||
@@ -3938,7 +3838,7 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
var point = e.GetPosition(ComponentLibraryWindow);
|
||||
if (point.Y > 40) // 閺嶅洭顣介弽蹇涚彯鎼达妇瀹虫稉?0px
|
||||
if (point.Y > 40) // 闂傚倷绀侀幖顐ょ矓閺夋嚚娲敇椤兘鍋撻崒娑氼浄閻庯綆浜滈崬銊╂椤愩垺澶勭紒瀣崄閵囨劙顢涢悙鑼啇闁哄鐗婇崕鎶姐€呴鍕€电痪顓炴噺閻濐亞绱?0px
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ using LanMountainDesktop.Theme;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class MainWindow
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private static readonly TimeSpan DesktopEditCommitAnimationDuration = FluttermotionToken.Standard;
|
||||
private static readonly TimeSpan DesktopEditCancelAnimationDuration = FluttermotionToken.Fast;
|
||||
@@ -123,7 +123,7 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
_componentLibraryCollapsePresenter.SyncExpandedState(ComponentLibraryWindow.Margin, ComponentLibraryWindow.Opacity);
|
||||
_componentLibraryCollapsePresenter.SyncExpandedState(ComponentLibraryWindow.Margin);
|
||||
}
|
||||
|
||||
private void CollapseComponentLibraryForDesktopEdit(string? title)
|
||||
|
||||
@@ -22,7 +22,7 @@ using LanMountainDesktop.Theme;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class MainWindow
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private const int MinDesktopPageCount = 1;
|
||||
private const int MaxDesktopPageCount = 12;
|
||||
@@ -68,6 +68,7 @@ public partial class MainWindow
|
||||
private long _desktopSwipeLastTimestamp;
|
||||
private double _desktopSwipeVelocityX;
|
||||
private double _desktopSwipeBaseOffset;
|
||||
private int? _desktopSwipePointerId;
|
||||
private bool _desktopPageContextInitialized;
|
||||
private bool _desktopPageContextEditMode;
|
||||
private int _desktopPageContextActiveMask;
|
||||
@@ -75,7 +76,7 @@ public partial class MainWindow
|
||||
private int? _desktopPageContextSettlingTargetIndex;
|
||||
private int _desktopPageContextSettleRevision;
|
||||
|
||||
// 三指滑动/右键拖动相关
|
||||
// 婵犵數鍋為崹鍫曞箰閹间絸鍥箥椤旂懓浜鹃柛顭戝亯婢规ɑ銇勯婊冨妤犵偛顑呴埞鎴﹀窗?闂傚倷绀侀幉锟犳偡閿旂晫绠惧┑鐘叉搐閺嬩焦銇勯幘鍗炵仼缂佺媭鍨堕弻鈥崇暤椤旂厧鏁俊銈呮噺閻撶喖鏌嶉崫鍕灓闁绘帡绠栭弻?
|
||||
private bool _isThreeFingerOrRightDragSwipeActive;
|
||||
private readonly HashSet<int> _activePointerIds = [];
|
||||
|
||||
@@ -264,7 +265,6 @@ public partial class MainWindow
|
||||
Grid.SetColumn(LauncherPagePanel, 1);
|
||||
Grid.SetRow(LauncherPagePanel, 0);
|
||||
|
||||
// 为启动台添加安全边距以确保圆角不被裁剪
|
||||
var launcherMargin = Math.Clamp(gridMetrics.CellSize * 0.15, 6, 16);
|
||||
LauncherPagePanel.Margin = new Thickness(launcherMargin);
|
||||
LauncherPagePanel.Width = Math.Max(1, pageWidth - launcherMargin * 2);
|
||||
@@ -272,7 +272,7 @@ public partial class MainWindow
|
||||
LauncherPagePanel.MaxWidth = pageWidth - launcherMargin * 2;
|
||||
LauncherPagePanel.MaxHeight = pageHeight - launcherMargin * 2;
|
||||
|
||||
// 更新启动台图标布局
|
||||
// 闂傚倷绀侀幖顐⒚洪妶澶嬪仱闁靛ň鏅涢拑鐔封攽閻樺弶鎼愰悷娆欓檮閵囧嫰寮介妸銊ヮ棟閻炴氨鍠栧娲川婵犲嫭鍣┑鐘灪閿氶棁澶嬫叏濡炶浜鹃悗娈垮枙缁瑥鐣烽幆閭︽Ь濡炪倕绻戦幐鎶藉箖濮椻偓閹瑩鍩℃担宄邦棜
|
||||
UpdateLauncherTileLayout();
|
||||
|
||||
_desktopSurfacePageWidth = pageWidth;
|
||||
@@ -287,38 +287,29 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取启动台面板的实际可用宽度(减去Padding)
|
||||
var availableWidth = Math.Max(1, LauncherPagePanel.Bounds.Width - 36); // 18px padding on each side
|
||||
var availableHeight = Math.Max(1, LauncherPagePanel.Bounds.Height - 100); // 预留标题空间
|
||||
var availableHeight = Math.Max(1, LauncherPagePanel.Bounds.Height - 100); // 婵犵妲呴崑鍛熆濡皷鍋撳鐓庢珝鐎殿喗濞婇崺鈧い鎺戝閻撴稓鈧箍鍎遍幊蹇涘窗濡眹浜滈柨婵嗘处濞呮洜绱掗鍊熷閻撱倖銇勮箛鎾村珔缂?
|
||||
|
||||
if (availableWidth <= 1 || availableHeight <= 1)
|
||||
{
|
||||
// 如果尺寸还未计算,使用默认值
|
||||
availableWidth = 600;
|
||||
// 婵犵數濮烽。浠嬪焵椤掆偓閸熷潡鍩€椤掆偓缂嶅﹪骞冨Ο璇茬窞閻忕偠鍋愰崜銊╂⒑閸涘﹦绠撻悗姘卞厴瀹曠敻鎮㈤悡搴i獓闂佸啿鎼导鎺楀箣濠垫捁鈧寧銇勯幘璺盒e┑顖氥偢閺屻劌鈽夊Ο渚紑闂佸搫妫崜鐔煎蓟閵娿儮妲堟俊顖欒濞堫厽绻濋悽闈涗粶婵炲樊鍙冮獮鍐╃鐎n€晠鏌嶉崫鍕殭缂佹绻濋弻锝夋偐闁秵顎栭梺绋匡攻濞茬喖宕洪埀? availableWidth = 600;
|
||||
availableHeight = 400;
|
||||
}
|
||||
|
||||
// 计算最佳图标尺寸
|
||||
// 目标:每行显示4-8个图标,根据屏幕宽度调整
|
||||
// 闂備浇宕垫慨宕囨閵堝洦顫曢柡鍥ュ灪閸嬧晛鈹戦悩瀹犲閻庢艾顦甸弻宥堫檨闁告挻宀搁獮蹇涘川閺夋垹顦ㄩ梺鍛婄懃椤﹂亶銆呴銏♀拺闁告繂瀚瓭濠电偛鐪伴崐婵嗩嚕娴兼潙纾兼繝褎鍎虫禍? // 闂傚倷鑳堕崕鐢稿疾閳哄懎绐楁俊銈呮噺閸嬪鏌ㄥ┑鍡╂Ч闁哄拋鍓氶幈銊ヮ潨閸℃绠诲┑鈥崇湴閸旀垿骞冪捄琛℃婵☆垳绮幏鍗炩攽閳藉棗鐏犳い锕佷含閸?-8婵犵數鍋為崹鍫曞箹閳哄倻顩叉繝濠傚幘閻熼偊娼ㄩ柍褜鍓欓锝嗙鐎n亞鍊炴俊鐐差儏濞寸兘藝椤曗偓濮婃椽宕崟顓夈儲銇勯銏╂Ц闁伙絽鐏氶幏鍛姜閻楀牆濯伴梻濠庡亜濞诧箓骞愭ィ鍐炬晩閹兼番鍔嶉崐鐢电棯椤撶偞鍣烘い銉ヮ樀閹鎮烽幍顕嗙礊闂佺懓顨庨崑濠傜暦濮椻偓閸╋繝宕掑☉鍗炴櫔
|
||||
const int minColumns = 4;
|
||||
const int maxColumns = 8;
|
||||
const double targetAspectRatio = 1.2; // 图标宽高比
|
||||
|
||||
// 计算每列可以显示的图标数量
|
||||
const double targetAspectRatio = 1.2; // 闂傚倷鐒﹂幃鍫曞磿閹惰棄纾婚柟鍓х帛閸嬪鏌ㄥ┑鍡樼闁稿鎹囬弻鍛槈濮樿京鍘梻浣虹帛缁诲秹宕伴弽顒夋毎?
|
||||
var optimalColumnCount = Math.Clamp((int)Math.Floor(availableWidth / 120), minColumns, maxColumns);
|
||||
|
||||
// 根据列数计算图标尺寸
|
||||
var tileWidth = Math.Floor(availableWidth / optimalColumnCount) - 12; // 12px spacing
|
||||
var tileHeight = Math.Min(tileWidth / targetAspectRatio, availableHeight / 4); // 至少显示4行
|
||||
|
||||
// 确保最小尺寸
|
||||
tileWidth = Math.Max(tileWidth, 100);
|
||||
var tileHeight = Math.Min(tileWidth / targetAspectRatio, availableHeight / 4); // 闂傚倷鑳堕崢褔宕查弻銉ョ柈闁秆勵殕閸庡秵銇勯弽顐粶闁告瑥锕弻娑㈠箻濡炵偓顦风紒?闂?
|
||||
// 缂傚倷鑳堕搹搴ㄥ矗鎼淬劌绐楅柡鍥╁У瀹曞弶鎱ㄥΟ鎸庣【閻庢艾顦甸弻宥堫檨闁告挻绋掔粋宥咁潰瀹€鈧悿鈧梺瑙勫劤閻°劑锝為崨瀛樼厽? tileWidth = Math.Max(tileWidth, 100);
|
||||
tileHeight = Math.Max(tileHeight, 80);
|
||||
|
||||
// 更新WrapPanel的Item尺寸
|
||||
LauncherRootTilePanel.Width = availableWidth;
|
||||
// 闂傚倷绀侀幖顐⒚洪妶澶嬪仱闁靛ň鏅涢拑鐔封攽閸屻倖杈渁pPanel闂傚倷鐒﹂惇褰掑礉瀹€鍕惞婵帞妫渕闂備浇顕х换鎰崲閹版澘绠规い鎰跺瘜閺? LauncherRootTilePanel.Width = availableWidth;
|
||||
|
||||
// 更新所有子元素的尺寸
|
||||
// 闂傚倷绀侀幖顐⒚洪妶澶嬪仱闁靛ň鏅涢拑鐔封攽閻樺弶鎼愮紒鐘劦閺屽秷顧侀柛鎾跺枎椤曪綁宕归銏㈢獮婵犵數濮寸€氼參骞夐妶澶嬧拺缂佸娉曠粻浼存煕閻旂顥嬬紒顔肩墕閻f繈宕熼鈧崜顓㈡⒑閸涘﹥澶勯柛瀣噹鍗遍柍褜鍓熼弻?
|
||||
foreach (var child in LauncherRootTilePanel.Children)
|
||||
{
|
||||
if (child is Button button)
|
||||
@@ -487,7 +478,7 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果在组件编辑模式下点击空白区域,取消选中(组件或启动台图标)
|
||||
// 婵犵數濮烽。浠嬪焵椤掆偓閸熷潡鍩€椤掆偓缂嶅﹪骞冨Ο璇茬窞闁归偊鍓氬畵宥夋⒑闂堟丹娑㈠川椤栨粌甯掓繝鐢靛仜椤曨厽鎱ㄧ€涙ɑ娅犻幖杈剧稻椤洘銇勮箛鎾村櫤缂傚秴娲弻鐔衡偓鐢告櫜鏉╃懓霉閿濆懎顥忛柛銈嗘礋閻擃偊宕惰閹癸綁鏌i悢鍛婂磳闁哄矉缍侀獮鍥敊閽樺鐣梻浣规偠閸娿倝宕板鍗炲灊婵鍩栭幆鐐烘偡濞嗗繐顏村ù鐘讳憾濮婃椽宕ㄦ繝鍕吂闂佸湱鈷堥崑濠囧箖閳ユ枼鏋庨柟鎯х摠濞呮牠鏌h箛鏇炰哗婵☆偄瀚濠囧箰鎼达絿顔曢梺鐟扮摠缁诲嫭鏅堕敃鍌涚厓鐟滄粓宕滃▎鎾嶅洭顢氶埀顒勫箠濞嗘挸绠i柨鏃囧Г濞呮牠姊洪崜鎻掍簴闁搞劌顭烽幆宀€鈧綆鈧垹缍婇幃鈺呭传閸曨厼甯块梻浣规偠閸斿﹪宕濋幋婵堟殾闁靛鏅╅弫宥嗘叏濮楀棗鍔俊銈呮噺閻撴洘绻涢崱妯哄缂佽泛寮剁换娑氣偓娑欙公閼拌法鈧鍠曠划娆忕暦閼告妲归幖杈剧秵濡?
|
||||
if (_isComponentLibraryOpen &&
|
||||
(_selectedDesktopComponentHost is not null || _selectedLauncherTileButton is not null))
|
||||
{
|
||||
@@ -504,7 +495,6 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查三指滑动功能是否启用
|
||||
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
var isThreeFingerSwipeEnabled = appSnapshot.EnableThreeFingerSwipe;
|
||||
|
||||
@@ -513,23 +503,21 @@ public partial class MainWindow
|
||||
var isRightButtonPressed = currentPoint.Properties.IsRightButtonPressed;
|
||||
var isLeftButtonPressed = currentPoint.Properties.IsLeftButtonPressed;
|
||||
|
||||
// 处理三指滑动/右键拖动模式
|
||||
// 婵犵數濮伴崹鐓庘枖濞戞埃鍋撳鐓庢珝妤犵偛鍟换婵嬪礃椤忎焦鐏冨┑鐘灱濞夋盯顢栭崨瀛樺剨閻熸瑥瀚弧鈧繝鐢靛Т閸燁偊鎮橀妷銉㈡斀?闂傚倷绀侀幉锟犳偡閿旂晫绠惧┑鐘叉搐閺嬩焦銇勯幘鍗炵仼缂佺媭鍨堕弻鈥崇暤椤旂厧鏁俊銈勬缁诲棙銇勯弽銊d粶闁稿鎸搁悾鐑藉炊閳哄﹥鏁?
|
||||
if (isThreeFingerSwipeEnabled)
|
||||
{
|
||||
// 跟踪活跃指针
|
||||
if (isLeftButtonPressed || isRightButtonPressed)
|
||||
{
|
||||
_activePointerIds.Add(pointerId);
|
||||
}
|
||||
|
||||
// 判断是否是三指滑动或右键拖动
|
||||
var isThreeFinger = _activePointerIds.Count >= 3;
|
||||
var isRightDrag = isRightButtonPressed;
|
||||
|
||||
if (isThreeFinger || isRightDrag)
|
||||
{
|
||||
// 三指/右键拖动模式:跳过所有组件交互检查,直接开始滑动
|
||||
ClearDesktopPageContextSettle(refreshContext: false);
|
||||
// 婵犵數鍋為崹鍫曞箰閹间絸鍥箥椤旂懓浜?闂傚倷绀侀幉锟犳偡閿旂晫绠惧┑鐘叉搐閺嬩焦銇勯幘鍗炵仼缂佺媭鍨堕弻鈥崇暤椤旂厧鏁俊銈勬缁诲棙銇勯弽銊d粶闁稿鎸搁悾鐑藉炊閳哄﹥鏁ら梻鍌欑劍鐎笛呯矙閹烘挾鈹嶆繛宸簼閸婂鏌ㄩ弮鍥撳ù婧垮€濋弻娑㈠Ψ閿濆懎顬堝銈忕稻閻擄繝寮婚敓鐘查唶婵犲灚鍔栨缂傚倷绶¢崰鏍矓閻㈢數鐭夐柟鐑橆殔鐎氬鏌涢…鎴濅簻闁衡偓椤撶喓绠鹃悗娑欘焽閻鎮介娑辨疁閽樼喖鏌涘☉娆愮稇闁藉啰鍠栭弻鏇熷緞濡櫣浠紓浣插亾濠㈣埖鍔栭悡鐔兼煃鏉炴媽鍏岄柟鐣屽█閹粙顢涘☉娆戠▏濡炪倖娲╃紞渚€宕洪埀顒併亜閹哄秶鍔嶉柛娆忕箻閹鏁愭惔鈥茬敖闂佽鐏氶崝鎴﹀蓟? ClearDesktopPageContextSettle(refreshContext: false);
|
||||
_isThreeFingerOrRightDragSwipeActive = true;
|
||||
_isDesktopSwipeActive = true;
|
||||
_isDesktopSwipeDirectionLocked = false;
|
||||
@@ -539,14 +527,15 @@ public partial class MainWindow
|
||||
_desktopSwipeVelocityX = 0;
|
||||
_desktopSwipeLastTimestamp = Stopwatch.GetTimestamp();
|
||||
_desktopSwipeBaseOffset = -_currentDesktopSurfaceIndex * _desktopSurfacePageWidth;
|
||||
|
||||
// 标记事件已处理,防止组件响应
|
||||
_desktopSwipePointerId = pointerId;
|
||||
e.Handled = true;
|
||||
|
||||
// 闂傚倷绀侀幖顐ょ矓閺夋嚚娲煛閸滀焦鏅╅梺鎼炲劘閸斿酣銆呴弻銉﹀€甸柨婵嗗€瑰▍鍡樸亜閹邦喗娅曢柍褜鍓涢幊鎾诲箟闄囬妵鎰板礃椤斻垹娲崺锟犲川椤旈棿鍝楅梻浣虹《濡插懘宕㈤崜褏鐭嗗鑸靛姈閳锋帡鏌涢幇鈺佸缂佺嫏鍕╀簻闁圭儤鎸鹃妴鎺旂磼鏉堛劌娴€规洜鍠栭、鏃堝椽娴i晲缂撻梻鍌欑閹诧紕鎹㈤崒婊呯煋閻庡灚鐡曟慨? e.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 原有单指滑动逻辑
|
||||
// 闂傚倷绀侀幉锟犫€﹂崶顒€绐楅柟閭﹀墾閼板灝銆掑锝呬壕閻庤娲╃换婵嗩嚕閹绢喗鍋勫瀣閳诲本绻濋悽闈浶㈤柨鏇樺劦瀹曞綊宕归锝呭伎闂佸啿鎼幊蹇涙倿婵犳碍鐓涢柛鏇ㄥ亞缁犳娊鎮? if (IsInteractivePointerSource(e.Source))
|
||||
if (IsInteractivePointerSource(e.Source))
|
||||
{
|
||||
return;
|
||||
@@ -571,6 +560,13 @@ public partial class MainWindow
|
||||
_desktopSwipeVelocityX = 0;
|
||||
_desktopSwipeLastTimestamp = Stopwatch.GetTimestamp();
|
||||
_desktopSwipeBaseOffset = -_currentDesktopSurfaceIndex * _desktopSurfacePageWidth;
|
||||
_desktopSwipePointerId = pointerId;
|
||||
}
|
||||
|
||||
private bool IsDesktopSwipePointer(IPointer? pointer)
|
||||
{
|
||||
return !_desktopSwipePointerId.HasValue ||
|
||||
pointer is not null && pointer.Id == _desktopSwipePointerId.Value;
|
||||
}
|
||||
|
||||
private static bool IsInteractivePointerSource(object? source)
|
||||
@@ -752,6 +748,11 @@ public partial class MainWindow
|
||||
|
||||
private void OnDesktopPagesPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (_isDesktopSwipeActive && !IsDesktopSwipePointer(e.Pointer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isDesktopSwipeActive || !TryGetPointerPositionInDesktopViewport(e, out var pointerInViewport))
|
||||
{
|
||||
return;
|
||||
@@ -811,9 +812,13 @@ public partial class MainWindow
|
||||
|
||||
private void OnDesktopPagesPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
// 清理活跃指针
|
||||
var pointerId = e.Pointer?.Id ?? 0;
|
||||
_activePointerIds.Remove(pointerId);
|
||||
|
||||
if (_isDesktopSwipeActive && !IsDesktopSwipePointer(e.Pointer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (EndDesktopSwipeInteraction(e.Pointer))
|
||||
{
|
||||
@@ -823,10 +828,19 @@ public partial class MainWindow
|
||||
|
||||
private void OnDesktopPagesPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
|
||||
{
|
||||
// 清理活跃指针
|
||||
var pointerId = e.Pointer?.Id ?? 0;
|
||||
_activePointerIds.Remove(pointerId);
|
||||
|
||||
|
||||
if (!_isDesktopSwipeActive || !IsDesktopSwipePointer(e.Pointer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Pointer?.Captured == DesktopPagesViewport)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EndDesktopSwipeInteraction(e.Pointer);
|
||||
}
|
||||
|
||||
@@ -847,6 +861,7 @@ public partial class MainWindow
|
||||
_isDesktopSwipeDirectionLocked = false;
|
||||
_isThreeFingerOrRightDragSwipeActive = false;
|
||||
_activePointerIds.Clear();
|
||||
_desktopSwipePointerId = null;
|
||||
_desktopSwipeVelocityX = 0;
|
||||
_desktopSwipeLastTimestamp = 0;
|
||||
if (wasDirectionLocked)
|
||||
@@ -869,6 +884,7 @@ public partial class MainWindow
|
||||
_isDesktopSwipeDirectionLocked = false;
|
||||
_isThreeFingerOrRightDragSwipeActive = false;
|
||||
_activePointerIds.Clear();
|
||||
_desktopSwipePointerId = null;
|
||||
|
||||
if (pointer?.Captured == DesktopPagesViewport)
|
||||
{
|
||||
@@ -898,13 +914,12 @@ public partial class MainWindow
|
||||
var hasDistanceIntent = absDeltaX >= distanceThreshold && absDeltaX > absDeltaY * 1.05;
|
||||
var hasVelocityIntent = Math.Abs(_desktopSwipeVelocityX) >= velocityThreshold;
|
||||
|
||||
// 检查:三指/右键拖动 && 在第一页 && 向右滑动
|
||||
// 濠电姷顣藉Σ鍛村磻閳ь剟鏌涚€n偅宕岄柡宀嬬磿娴狅妇鎷犻幓鎺懶ョ紓鍌欐祰娴滎剚鏅跺Δ鍐煓濠㈣泛顑呯欢鐐烘倵閿濆簼绨芥俊?闂傚倷绀侀幉锟犳偡閿旂晫绠惧┑鐘叉搐閺嬩焦銇勯幘鍗炵仼缂佺媭鍨堕弻鈥崇暤椤旂厧鏁?&& 闂傚倷绶氬鑽ゆ嫻閻旂厧绀夐幖鎼厛閺佸嫰鏌涢妷锝呭闁崇粯妫冮弻宥堫檨闁告挻宀告俊?&& 闂傚倷绀侀幉锛勫枈瀹ュ鍨傚ù锝呭暔娴滃湱绱掔€n偒鍎ラ柣鎾卞劦閺岀喓鈧稒顭囩粻鎾舵偖?
|
||||
if (wasThreeFingerOrRightDrag &&
|
||||
_currentDesktopSurfaceIndex == 0 &&
|
||||
deltaX > 0 && // 向右滑动
|
||||
deltaX > 0 && // 闂傚倷绀侀幉锛勫枈瀹ュ鍨傚ù锝呭暔娴滃湱绱掔€n偒鍎ラ柣鎾卞劦閺岀喓鈧稒顭囩粻鎾舵偖?
|
||||
(hasDistanceIntent || hasVelocityIntent))
|
||||
{
|
||||
// 最小化到 Windows 桌面
|
||||
if (Application.Current is App app)
|
||||
{
|
||||
app.HideMainWindowToTray(this, "ThreeFingerOrRightDragSwipe");
|
||||
@@ -998,8 +1013,7 @@ public partial class MainWindow
|
||||
string.Empty));
|
||||
}
|
||||
|
||||
// 在图标渲染完成后,应用布局计算
|
||||
Dispatcher.UIThread.Post(() => UpdateLauncherTileLayout(), DispatcherPriority.Background);
|
||||
// 闂傚倷绶氬鑽ゆ嫻閻旂厧绀夐悘鐐电叓閻熼偊娼ㄩ柍褜鍓欓锝嗙鐎n亞鍊為梺闈涱煬閻撳牆煤椤掑嫭鈷戦柛婵嗗濠€浼存煙閸涘﹥鍊愰柟顕€绠栭、妤呭礋椤愩値鍚呴梻浣哥秺閸嬪﹪宕滃璺虹9闁汇垹鎲¢悡銉︾箾閹寸儐鐒鹃悗姘缁辨帡濡搁敂鎯у绩闂佽鍠曠划娆愪繆閹间礁唯鐟滄粍瀵煎畝鍕厽闊洦娲栨禍褰掓煕鐎n偅宕岄柟顔款潐缁楃喐绻濋崟顓ㄧ吹闂? Dispatcher.UIThread.Post(() => UpdateLauncherTileLayout(), DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private Button CreateLauncherFolderTile(StartMenuFolderNode folder)
|
||||
@@ -1063,8 +1077,7 @@ public partial class MainWindow
|
||||
BorderThickness = new Thickness(0),
|
||||
Margin = new Thickness(0, 0, 12, 12),
|
||||
CornerRadius = new CornerRadius(20),
|
||||
Child = panel
|
||||
// 不设置固定 Width 和 Height,由 UpdateLauncherTileLayout 动态设置
|
||||
Child = panel,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1145,11 +1158,8 @@ public partial class MainWindow
|
||||
BorderBrush = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(20),
|
||||
Padding = new Thickness(10),
|
||||
Content = content
|
||||
// 不设置固定 Width 和 Height,由 UpdateLauncherTileLayout 动态设置
|
||||
Content = content,
|
||||
};
|
||||
|
||||
// 根据设置决定是否显示背景
|
||||
if (_showLauncherTileBackground)
|
||||
{
|
||||
button.Classes.Add("glass-panel");
|
||||
@@ -1415,7 +1425,7 @@ public partial class MainWindow
|
||||
: fileName;
|
||||
}
|
||||
|
||||
private SettingsExpanderItem CreateLauncherHiddenItemRow(LauncherHiddenItemView hiddenItem)
|
||||
private FASettingsExpanderItem CreateLauncherHiddenItemRow(LauncherHiddenItemView hiddenItem)
|
||||
{
|
||||
var typeText = hiddenItem.Kind == LauncherEntryKind.Folder
|
||||
? L("settings.launcher.hidden_type_folder", "Folder")
|
||||
@@ -1430,7 +1440,7 @@ public partial class MainWindow
|
||||
BorderThickness = new Thickness(0),
|
||||
Tag = new LauncherHiddenItemToken(hiddenItem.Kind, hiddenItem.Key)
|
||||
};
|
||||
restoreButton.Content = new FluentIcons.Avalonia.Fluent.SymbolIcon
|
||||
restoreButton.Content = new FluentIcons.Avalonia.SymbolIcon
|
||||
{
|
||||
Symbol = FluentIcons.Common.Symbol.Eye,
|
||||
IconVariant = FluentIcons.Common.IconVariant.Regular,
|
||||
@@ -1441,7 +1451,7 @@ public partial class MainWindow
|
||||
ToolTip.SetTip(restoreButton, L("settings.launcher.restore_button", "Unhide"));
|
||||
restoreButton.Click += OnRestoreLauncherHiddenItemClick;
|
||||
|
||||
return new SettingsExpanderItem
|
||||
return new FASettingsExpanderItem
|
||||
{
|
||||
Content = hiddenItem.DisplayName,
|
||||
Description = typeText,
|
||||
@@ -1451,23 +1461,17 @@ public partial class MainWindow
|
||||
};
|
||||
}
|
||||
|
||||
private IconSource CreateLauncherHiddenItemIconSource(LauncherHiddenItemView hiddenItem)
|
||||
private FAIconSource? CreateLauncherHiddenItemIconSource(LauncherHiddenItemView hiddenItem)
|
||||
{
|
||||
if (hiddenItem.IconBitmap is not null)
|
||||
{
|
||||
return new ImageIconSource
|
||||
return new FAImageIconSource
|
||||
{
|
||||
Source = hiddenItem.IconBitmap
|
||||
};
|
||||
}
|
||||
|
||||
return new FluentIcons.Avalonia.Fluent.SymbolIconSource
|
||||
{
|
||||
Symbol = hiddenItem.Kind == LauncherEntryKind.Folder
|
||||
? FluentIcons.Common.Symbol.Folder
|
||||
: FluentIcons.Common.Symbol.Apps,
|
||||
IconVariant = FluentIcons.Common.IconVariant.Regular
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnRestoreLauncherHiddenItemClick(object? sender, RoutedEventArgs e)
|
||||
@@ -1696,7 +1700,7 @@ public partial class MainWindow
|
||||
Content = content
|
||||
};
|
||||
|
||||
// 根据设置决定是否显示背景
|
||||
// 闂傚倷绀侀幖顐ょ矓閻戞枻缍栧璺猴功閺嗐倕霉閿濆洤鍔嬪┑顖氥偢閺屾盯骞樺Δ鈧幊蹇涙倵椤撱垺鈷戦柛娑橈工婵洭鏌涢悢閿嬪仴闁诡喚鍋撻妶锝夊礃閵娿儱鎸ゆ俊鐐€栭悧妤冨枈瀹ュ纾垮┑鐘叉处閻撴盯鏌涢弴銊ヤ簻闁抽攱妫冮弻鏇㈠炊閵娿儱鎽甸梺纭呮珪椤ㄥ牊绂掗敃鍌涘€锋い鎺戝€哥拋?
|
||||
if (_showLauncherTileBackground)
|
||||
{
|
||||
button.Classes.Add("glass-panel");
|
||||
@@ -1775,7 +1779,7 @@ public partial class MainWindow
|
||||
Content = content
|
||||
};
|
||||
|
||||
// 根据设置决定是否显示背景
|
||||
// 闂傚倷绀侀幖顐ょ矓閻戞枻缍栧璺猴功閺嗐倕霉閿濆洤鍔嬪┑顖氥偢閺屾盯骞樺Δ鈧幊蹇涙倵椤撱垺鈷戦柛娑橈工婵洭鏌涢悢閿嬪仴闁诡喚鍋撻妶锝夊礃閵娿儱鎸ゆ俊鐐€栭悧妤冨枈瀹ュ纾垮┑鐘叉处閻撴盯鏌涢弴銊ヤ簻闁抽攱妫冮弻鏇㈠炊閵娿儱鎽甸梺纭呮珪椤ㄥ牊绂掗敃鍌涘€锋い鎺戝€哥拋?
|
||||
if (_showLauncherTileBackground)
|
||||
{
|
||||
button.Classes.Add("glass-panel");
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class MainWindow
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private void UpdateCurrentRenderBackendStatus()
|
||||
{
|
||||
|
||||
@@ -20,13 +20,13 @@ using LanMountainDesktop.Views.Components;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class MainWindow
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private TextBlock? CurrentRenderBackendLabelTextBlock => this.FindControl<TextBlock>("CurrentRenderBackendLabelTextBlock");
|
||||
private TextBlock? CurrentRenderBackendValueTextBlock => this.FindControl<TextBlock>("CurrentRenderBackendValueTextBlock");
|
||||
private TextBlock? CurrentRenderBackendImplementationTextBlock => this.FindControl<TextBlock>("CurrentRenderBackendImplementationTextBlock");
|
||||
private ComboBox? TimeZoneComboBox => this.FindControl<ComboBox>("TimeZoneComboBox");
|
||||
private SettingsExpander? LauncherHiddenItemsSettingsExpander => this.FindControl<SettingsExpander>("LauncherHiddenItemsSettingsExpander");
|
||||
private FASettingsExpander? LauncherHiddenItemsSettingsExpander => this.FindControl<FASettingsExpander>("LauncherHiddenItemsSettingsExpander");
|
||||
private TextBlock? LauncherHiddenItemsEmptyTextBlock => this.FindControl<TextBlock>("LauncherHiddenItemsEmptyTextBlock");
|
||||
|
||||
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
|
||||
@@ -38,13 +38,12 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
// 组件实例范围的设置变更不应触发整个桌面重新加载(比如翻页保存图片索引)
|
||||
if (e.Scope == SettingsScope.ComponentInstance)
|
||||
// 缁勪欢瀹炰緥鑼冨洿鐨勮缃彉鏇翠笉搴旇Е鍙戞暣涓闈㈤噸鏂板姞杞斤紙姣斿缈婚〉淇濆瓨鍥剧墖绱㈠紩锛? if (e.Scope == SettingsScope.ComponentInstance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动台设置变化时,重新渲染启动台图标
|
||||
// 鍚姩鍙拌缃彉鍖栨椂锛岄噸鏂版覆鏌撳惎鍔ㄥ彴鍥炬爣
|
||||
if (e.Scope == SettingsScope.Launcher && e.ChangedKeys is { Count: > 0 })
|
||||
{
|
||||
var changedKeys = e.ChangedKeys.ToArray();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class MainWindow
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private bool _isSingleInstancePromptVisible;
|
||||
|
||||
@@ -38,14 +39,14 @@ public partial class MainWindow
|
||||
|
||||
try
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
var dialog = new FAContentDialog
|
||||
{
|
||||
Title = L("single_instance.notice.title", "应用已经运行"),
|
||||
Title = L("single_instance.notice.title", "Already running"),
|
||||
Content = L(
|
||||
"single_instance.notice.description",
|
||||
"应用已经运行,无需多次点击打开。"),
|
||||
PrimaryButtonText = L("single_instance.notice.button", "确定"),
|
||||
DefaultButton = ContentDialogButton.Primary
|
||||
"LanMountainDesktop is already running. The existing window will stay active, so no new instance was started."),
|
||||
PrimaryButtonText = L("single_instance.notice.button", "OK"),
|
||||
DefaultButton = FAContentDialogButton.Primary
|
||||
};
|
||||
|
||||
await dialog.ShowAsync(this);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:ic="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:ic="using:FluentIcons.Avalonia"
|
||||
xmlns:mi="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:comp="using:LanMountainDesktop.Views.Components"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
x:Class="LanMountainDesktop.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
WindowState="FullScreen"
|
||||
SystemDecorations="None"
|
||||
WindowDecorations="None"
|
||||
CanResize="False"
|
||||
UseLayoutRounding="True"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
@@ -225,18 +225,10 @@
|
||||
<Canvas x:Name="DesktopEditDragLayer"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<Canvas x:Name="ComponentPreviewStagingHost"
|
||||
Width="1"
|
||||
Height="1"
|
||||
Opacity="0"
|
||||
ClipToBounds="True"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
IsHitTestVisible="False" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 状态栏阴影层 - macOS 风格的完整阴影带 -->
|
||||
<!-- 鐘舵€佹爮闃村奖灞?- macOS 椋庢牸鐨勫畬鏁撮槾褰卞甫 -->
|
||||
<Border x:Name="StatusBarOverlay"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
@@ -265,7 +257,7 @@
|
||||
Padding="4"
|
||||
ZIndex="2">
|
||||
<Grid ColumnDefinitions="*,Auto,*">
|
||||
<!-- 左侧状态栏组件 -->
|
||||
<!-- 宸︿晶鐘舵€佹爮缁勪欢 -->
|
||||
<StackPanel x:Name="TopStatusLeftPanel"
|
||||
Grid.Column="0"
|
||||
Orientation="Horizontal"
|
||||
@@ -281,7 +273,7 @@
|
||||
IsVisible="False"
|
||||
Margin="0" />
|
||||
</StackPanel>
|
||||
<!-- 中间状态栏组件 -->
|
||||
<!-- 涓棿鐘舵€佹爮缁勪欢 -->
|
||||
<StackPanel x:Name="TopStatusCenterPanel"
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
@@ -297,7 +289,7 @@
|
||||
IsVisible="False"
|
||||
Margin="0" />
|
||||
</StackPanel>
|
||||
<!-- 右侧状态栏组件 -->
|
||||
<!-- 鍙充晶鐘舵€佹爮缁勪欢 -->
|
||||
<StackPanel x:Name="TopStatusRightPanel"
|
||||
Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
@@ -627,7 +619,7 @@
|
||||
<Border x:Name="ComponentLibraryWindow"
|
||||
IsVisible="False"
|
||||
Opacity="0"
|
||||
Classes="surface-translucent-strong"
|
||||
Background="Transparent"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Width="620"
|
||||
@@ -636,8 +628,6 @@
|
||||
Height="320"
|
||||
MinHeight="260"
|
||||
Margin="24,24,24,100"
|
||||
CornerRadius="36"
|
||||
Padding="14"
|
||||
PointerPressed="OnComponentLibraryWindowPointerPressed"
|
||||
PointerMoved="OnComponentLibraryWindowPointerMoved"
|
||||
PointerReleased="OnComponentLibraryWindowPointerReleased">
|
||||
@@ -647,142 +637,146 @@
|
||||
</Transitions>
|
||||
</Border.Transitions>
|
||||
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
RowSpacing="10">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="ComponentLibraryTitleTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="Widgets" />
|
||||
<Button x:Name="CloseComponentLibraryButton"
|
||||
Grid.Column="1"
|
||||
Padding="8"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Click="OnCloseComponentLibraryClick">
|
||||
<fi:SymbolIcon Classes="icon-s"
|
||||
Symbol="Dismiss"
|
||||
IconVariant="Regular" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="1"
|
||||
Classes="surface-translucent-panel"
|
||||
CornerRadius="12"
|
||||
Padding="14">
|
||||
<Grid>
|
||||
<!-- Category picker (outer) -->
|
||||
<Grid x:Name="ComponentLibraryCategoriesView">
|
||||
<Grid RowDefinitions="*">
|
||||
<Border x:Name="ComponentLibraryCategoryViewport"
|
||||
Background="Transparent"
|
||||
ClipToBounds="True">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<Grid x:Name="ComponentLibraryCategoryPagesHost"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top">
|
||||
<Grid x:Name="ComponentLibraryCategoryPagesContainer" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="ComponentLibraryEmptyTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="No components." />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Component picker (inner) -->
|
||||
<Grid x:Name="ComponentLibraryComponentsView"
|
||||
IsVisible="False"
|
||||
RowDefinitions="Auto,*"
|
||||
RowSpacing="10">
|
||||
<Button x:Name="ComponentLibraryBackButton"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Left"
|
||||
Padding="8,6"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Click="OnComponentLibraryBackClick">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<fi:SymbolIcon Classes="icon-s" Symbol="ArrowLeft" IconVariant="Regular" />
|
||||
<TextBlock x:Name="ComponentLibraryBackTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
Text="Back" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="8">
|
||||
<Button x:Name="ComponentLibraryPrevComponentButton"
|
||||
Grid.Column="0"
|
||||
Width="36"
|
||||
Height="36"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="18"
|
||||
Click="OnComponentLibraryPrevComponentClick"
|
||||
IsVisible="False">
|
||||
<fi:SymbolIcon Symbol="ChevronLeft"
|
||||
IconVariant="Regular" />
|
||||
</Button>
|
||||
|
||||
<Border x:Name="ComponentLibraryComponentViewport"
|
||||
Grid.Column="1"
|
||||
Background="Transparent"
|
||||
ClipToBounds="True"
|
||||
PointerPressed="OnComponentLibraryComponentViewportPointerPressed"
|
||||
PointerMoved="OnComponentLibraryComponentViewportPointerMoved"
|
||||
PointerReleased="OnComponentLibraryComponentViewportPointerReleased"
|
||||
PointerCaptureLost="OnComponentLibraryComponentViewportPointerCaptureLost">
|
||||
<Grid>
|
||||
<Grid x:Name="ComponentLibraryComponentPagesHost"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top">
|
||||
<Grid.RenderTransform>
|
||||
<TranslateTransform>
|
||||
<TranslateTransform.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="X" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
|
||||
</Transitions>
|
||||
</TranslateTransform.Transitions>
|
||||
</TranslateTransform>
|
||||
</Grid.RenderTransform>
|
||||
|
||||
<Grid x:Name="ComponentLibraryComponentPagesContainer" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Button x:Name="ComponentLibraryNextComponentButton"
|
||||
Grid.Column="2"
|
||||
Width="36"
|
||||
Height="36"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="18"
|
||||
Click="OnComponentLibraryNextComponentClick"
|
||||
IsVisible="False">
|
||||
<fi:SymbolIcon Symbol="ChevronRight"
|
||||
IconVariant="Regular" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Border Classes="surface-translucent-strong"
|
||||
CornerRadius="36"
|
||||
Padding="14">
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
RowSpacing="10">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="ComponentLibraryTitleTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="Widgets" />
|
||||
<Button x:Name="CloseComponentLibraryButton"
|
||||
Grid.Column="1"
|
||||
Padding="8"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Click="OnCloseComponentLibraryClick">
|
||||
<fi:SymbolIcon Classes="icon-s"
|
||||
Symbol="Dismiss"
|
||||
IconVariant="Regular" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="1"
|
||||
Classes="surface-translucent-panel"
|
||||
CornerRadius="12"
|
||||
Padding="14">
|
||||
<Grid>
|
||||
<!-- Category picker (outer) -->
|
||||
<Grid x:Name="ComponentLibraryCategoriesView">
|
||||
<Grid RowDefinitions="*">
|
||||
<Border x:Name="ComponentLibraryCategoryViewport"
|
||||
Background="Transparent"
|
||||
ClipToBounds="True">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<Grid x:Name="ComponentLibraryCategoryPagesHost"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top">
|
||||
<Grid x:Name="ComponentLibraryCategoryPagesContainer" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="ComponentLibraryEmptyTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||
Text="No components." />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Component picker (inner) -->
|
||||
<Grid x:Name="ComponentLibraryComponentsView"
|
||||
IsVisible="False"
|
||||
RowDefinitions="Auto,*"
|
||||
RowSpacing="10">
|
||||
<Button x:Name="ComponentLibraryBackButton"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Left"
|
||||
Padding="8,6"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Click="OnComponentLibraryBackClick">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<fi:SymbolIcon Classes="icon-s" Symbol="ArrowLeft" IconVariant="Regular" />
|
||||
<TextBlock x:Name="ComponentLibraryBackTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
Text="Back" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="8">
|
||||
<Button x:Name="ComponentLibraryPrevComponentButton"
|
||||
Grid.Column="0"
|
||||
Width="36"
|
||||
Height="36"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="18"
|
||||
Click="OnComponentLibraryPrevComponentClick"
|
||||
IsVisible="False">
|
||||
<fi:SymbolIcon Symbol="ChevronLeft"
|
||||
IconVariant="Regular" />
|
||||
</Button>
|
||||
|
||||
<Border x:Name="ComponentLibraryComponentViewport"
|
||||
Grid.Column="1"
|
||||
Background="Transparent"
|
||||
ClipToBounds="True"
|
||||
PointerPressed="OnComponentLibraryComponentViewportPointerPressed"
|
||||
PointerMoved="OnComponentLibraryComponentViewportPointerMoved"
|
||||
PointerReleased="OnComponentLibraryComponentViewportPointerReleased"
|
||||
PointerCaptureLost="OnComponentLibraryComponentViewportPointerCaptureLost">
|
||||
<Grid>
|
||||
<Grid x:Name="ComponentLibraryComponentPagesHost"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top">
|
||||
<Grid.RenderTransform>
|
||||
<TranslateTransform>
|
||||
<TranslateTransform.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="X" Duration="{StaticResource FluttermotionToken.Duration.Page}" />
|
||||
</Transitions>
|
||||
</TranslateTransform.Transitions>
|
||||
</TranslateTransform>
|
||||
</Grid.RenderTransform>
|
||||
|
||||
<Grid x:Name="ComponentLibraryComponentPagesContainer" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Button x:Name="ComponentLibraryNextComponentButton"
|
||||
Grid.Column="2"
|
||||
Width="36"
|
||||
Height="36"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
CornerRadius="18"
|
||||
Click="OnComponentLibraryNextComponentClick"
|
||||
IsVisible="False">
|
||||
<fi:SymbolIcon Symbol="ChevronRight"
|
||||
IconVariant="Regular" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="ComponentLibraryCollapsedChipHost"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
xmlns:vm="using:LanMountainDesktop.Views"
|
||||
x:Class="LanMountainDesktop.Views.NotificationDialogWindow"
|
||||
x:DataType="vm:NotificationDialogViewModel"
|
||||
SystemDecorations="None"
|
||||
WindowDecorations="None"
|
||||
Background="Transparent"
|
||||
ShowInTaskbar="False"
|
||||
Topmost="True"
|
||||
@@ -12,7 +12,6 @@
|
||||
SizeToContent="WidthAndHeight"
|
||||
TransparencyLevelHint="Transparent"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="NoChrome"
|
||||
ExtendClientAreaTitleBarHeightHint="-1">
|
||||
|
||||
<Border x:Name="DialogCard"
|
||||
|
||||
@@ -9,7 +9,6 @@ using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using FluentIcons.Avalonia;
|
||||
using FluentIcons.Avalonia.Fluent;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
x:Class="LanMountainDesktop.Views.NotificationWindow"
|
||||
x:DataType="vm:NotificationViewModel"
|
||||
SystemDecorations="None"
|
||||
WindowDecorations="None"
|
||||
Background="Transparent"
|
||||
ShowInTaskbar="False"
|
||||
Topmost="True"
|
||||
@@ -13,7 +13,6 @@
|
||||
SizeToContent="WidthAndHeight"
|
||||
TransparencyLevelHint="Transparent"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="NoChrome"
|
||||
ExtendClientAreaTitleBarHeightHint="-1">
|
||||
|
||||
<Window.Styles>
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage"
|
||||
x:DataType="vm:AboutSettingsPageViewModel">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<ImageBrush x:Key="AboutBannerBrush" Source="/Assets/about_banner_light.png" Stretch="Uniform" AlignmentX="Center" AlignmentY="Center" />
|
||||
<ImageBrush x:Key="AboutBannerBrush"
|
||||
Source="/Assets/about_banner_light.png"
|
||||
Stretch="Uniform"
|
||||
AlignmentX="Center"
|
||||
AlignmentY="Center" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<ImageBrush x:Key="AboutBannerBrush" Source="/Assets/about_banner_dark.png" Stretch="Uniform" AlignmentX="Center" AlignmentY="Center" />
|
||||
<ImageBrush x:Key="AboutBannerBrush"
|
||||
Source="/Assets/about_banner_dark.png"
|
||||
Stretch="Uniform"
|
||||
AlignmentX="Center"
|
||||
AlignmentY="Center" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
@@ -36,7 +44,7 @@
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|InfoBar.about-static-info">
|
||||
<Style Selector="ui|FAInfoBar.about-static-info">
|
||||
<Setter Property="IsOpen" Value="True" />
|
||||
<Setter Property="IsClosable" Value="False" />
|
||||
<Setter Property="Severity" Value="Informational" />
|
||||
@@ -57,30 +65,29 @@
|
||||
<TextBlock Classes="settings-subsection-title"
|
||||
Text="{Binding AppInfoHeader}" />
|
||||
|
||||
<ui:InfoBar Classes="about-static-info"
|
||||
Title="{Binding VersionLabel}"
|
||||
Message="{Binding VersionText}">
|
||||
<ui:InfoBar.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Info" />
|
||||
</ui:InfoBar.IconSource>
|
||||
</ui:InfoBar>
|
||||
<ui:FAInfoBar Classes="about-static-info"
|
||||
Title="{Binding VersionLabel}"
|
||||
Message="{Binding VersionText}">
|
||||
<ui:FAInfoBar.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰊈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FAInfoBar.IconSource>
|
||||
</ui:FAInfoBar>
|
||||
|
||||
<ui:InfoBar Classes="about-static-info"
|
||||
Title="{Binding CodenameLabel}"
|
||||
Message="{Binding CodenameText}">
|
||||
<ui:InfoBar.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Bookmark" />
|
||||
</ui:InfoBar.IconSource>
|
||||
</ui:InfoBar>
|
||||
<ui:FAInfoBar Classes="about-static-info"
|
||||
Title="{Binding CodenameLabel}"
|
||||
Message="{Binding CodenameText}">
|
||||
<ui:FAInfoBar.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󱏐" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FAInfoBar.IconSource>
|
||||
</ui:FAInfoBar>
|
||||
|
||||
<!-- 版权声明 - 放在渲染显示前面 -->
|
||||
<ui:SettingsExpander Header="版权声明"
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Document" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpanderItem>
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<ui:FASettingsExpander Header="Project resources"
|
||||
IsExpanded="True">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰊬" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<ui:FASettingsExpanderItem.Footer>
|
||||
<WrapPanel>
|
||||
<WrapPanel.Styles>
|
||||
<Style Selector="HyperlinkButton">
|
||||
@@ -89,18 +96,18 @@
|
||||
</Style>
|
||||
</WrapPanel.Styles>
|
||||
<HyperlinkButton NavigateUri="https://github.com/wwiinnddyy/LanMountainDesktop">
|
||||
<TextBlock Text="GitHub 仓库" />
|
||||
<TextBlock Text="GitHub Repository" />
|
||||
</HyperlinkButton>
|
||||
<HyperlinkButton NavigateUri="https://github.com/wwiinnddyy/LanMountainDesktop/issues">
|
||||
<TextBlock Text="问题反馈" />
|
||||
<TextBlock Text="Issue Tracker" />
|
||||
</HyperlinkButton>
|
||||
</WrapPanel>
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:FASettingsExpanderItem.Footer>
|
||||
<TextBlock>
|
||||
<Run Text="Copyright (c) 2024-" /><Run Text="2025" /> Lincube
|
||||
</TextBlock>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpanderItem>
|
||||
</ui:FASettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -74,6 +74,9 @@ public partial class AboutSettingsPage : SettingsPageBase
|
||||
|
||||
private void OnAboutHeroCardPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var elapsed = now - _lastHeroCardClickTime;
|
||||
|
||||
@@ -111,25 +114,23 @@ public partial class AboutSettingsPage : SettingsPageBase
|
||||
}
|
||||
else if (remaining <= 2)
|
||||
{
|
||||
Debug.WriteLine($"[AboutSettingsPage] 再点击 {remaining} 次即可启用开发者模式。");
|
||||
Debug.WriteLine($"[AboutSettingsPage] {remaining} tap(s) remaining before developer mode unlocks.");
|
||||
}
|
||||
}
|
||||
|
||||
private async void PromptEnableDevMode(ISettingsFacadeService settingsFacade)
|
||||
{
|
||||
var dialog = new ContentDialog
|
||||
var dialog = new FAContentDialog
|
||||
{
|
||||
Title = "启用开发者模式",
|
||||
Content = "开发者模式提供了插件调试、热重载等高级功能,仅供开发和调试用途。\n\n" +
|
||||
"请注意:开发者不对以非开发用途使用此功能造成的任何后果负责,也不接受以非开发用途使用时产生的 Bug 反馈。\n\n" +
|
||||
"确定要启用开发者模式吗?",
|
||||
PrimaryButtonText = "启用",
|
||||
CloseButtonText = "取消",
|
||||
DefaultButton = ContentDialogButton.Close
|
||||
Title = "Enable developer mode",
|
||||
Content = "Developer mode exposes experimental settings, diagnostics, and local plugin debugging options.\n\nUse it only when you are actively testing or troubleshooting the desktop host.",
|
||||
PrimaryButtonText = "Enable",
|
||||
CloseButtonText = "Not now",
|
||||
DefaultButton = FAContentDialogButton.Close
|
||||
};
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
if (result != ContentDialogResult.Primary)
|
||||
if (result != FAContentDialogResult.Primary)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.AppearanceSettingsPage"
|
||||
x:DataType="vm:AppearanceSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
@@ -13,12 +13,12 @@
|
||||
Text="{Binding ThemeHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<ui:SettingsExpander Header="{Binding ThemeModeLabel}"
|
||||
<ui:FASettingsExpander Header="{Binding ThemeModeLabel}"
|
||||
Description="{Binding ThemeModeDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="WeatherMoon" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰔄" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ComboBox Width="200"
|
||||
ItemsSource="{Binding ThemeModeOptions}"
|
||||
SelectedItem="{Binding SelectedThemeMode}">
|
||||
@@ -28,24 +28,24 @@
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{Binding UseSystemChromeLabel}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Window" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander Header="{Binding UseSystemChromeLabel}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰤈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding UseSystemChrome}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{Binding ThemeColorModeLabel}"
|
||||
<ui:FASettingsExpander Header="{Binding ThemeColorModeLabel}"
|
||||
Description="{Binding ThemeColorSourceDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="PaintBrush" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰅼" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ComboBox Width="240"
|
||||
ItemsSource="{Binding ThemeColorModes}"
|
||||
SelectedItem="{Binding SelectedThemeColorMode}">
|
||||
@@ -55,15 +55,15 @@
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{Binding SystemMaterialLabel}"
|
||||
<ui:FASettingsExpander Header="{Binding SystemMaterialLabel}"
|
||||
Description="{Binding SystemMaterialDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="WindowDevTools" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󱩈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ComboBox Width="220"
|
||||
ItemsSource="{Binding SystemMaterialModes}"
|
||||
SelectedItem="{Binding SelectedSystemMaterialMode}">
|
||||
@@ -73,15 +73,15 @@
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{Binding ThemeColorLabel}"
|
||||
<ui:FASettingsExpander Header="{Binding ThemeColorLabel}"
|
||||
Description="{Binding ThemeColorSourceDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Color" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpanderItem>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰢈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<StackPanel Spacing="12">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
@@ -257,8 +257,8 @@
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpanderItem>
|
||||
</ui:FASettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.ComponentsSettingsPage"
|
||||
x:DataType="vm:ComponentsSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
@@ -12,12 +12,12 @@
|
||||
Text="{Binding ComponentsHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<ui:SettingsExpander Header="{Binding ComponentsHeader}"
|
||||
<ui:FASettingsExpander Header="{Binding ComponentsHeader}"
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Apps" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpanderItem>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰀤" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="16">
|
||||
<TextBlock Text="{Binding ShortSideCellsLabel}"
|
||||
VerticalAlignment="Center" />
|
||||
@@ -33,8 +33,8 @@
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right" />
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem>
|
||||
<ui:SettingsExpanderItem>
|
||||
</ui:FASettingsExpanderItem>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="16">
|
||||
<TextBlock Text="{Binding EdgeInsetPercentLabel}"
|
||||
VerticalAlignment="Center" />
|
||||
@@ -50,8 +50,8 @@
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right" />
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem>
|
||||
<ui:SettingsExpanderItem>
|
||||
</ui:FASettingsExpanderItem>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Text="{Binding SpacingPresetLabel}"
|
||||
VerticalAlignment="Center" />
|
||||
@@ -66,19 +66,19 @@
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpanderItem>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<controls:IconText Icon="ShapeOrganic"
|
||||
Text="{Binding ComponentRadiusHeader}"
|
||||
Margin="0,12,0,4" />
|
||||
|
||||
<ui:SettingsExpander Header="{Binding CornerRadiusStyleLabel}"
|
||||
<ui:FASettingsExpander Header="{Binding CornerRadiusStyleLabel}"
|
||||
Description="{Binding CornerRadiusStyleDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="ShapeOrganic" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰿨" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<ComboBox Width="200"
|
||||
ItemsSource="{Binding CornerRadiusStyleOptions}"
|
||||
@@ -93,8 +93,8 @@
|
||||
<fi:SymbolIcon Symbol="QuestionCircle" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,84 +1,92 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.DevSettingsPage"
|
||||
x:DataType="vm:DevSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container settings-page-animated">
|
||||
|
||||
<ui:InfoBar IsOpen="True"
|
||||
<ui:FAInfoBar IsOpen="True"
|
||||
IsClosable="False"
|
||||
Severity="Warning"
|
||||
Title="开发者模式"
|
||||
Message="开发者模式仅供开发和调试用途。开发者不对以非开发用途使用此功能造成的任何后果负责。"
|
||||
Margin="0,0,0,16" />
|
||||
Title="Preview and developer features"
|
||||
Message="These options are intended for debugging, diagnostics, and local plugin development."
|
||||
Margin="0,0,0,16">
|
||||
<ui:FAInfoBar.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰊈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FAInfoBar.IconSource>
|
||||
</ui:FAInfoBar>
|
||||
|
||||
<ui:SettingsExpander Header="启用开发者模式"
|
||||
Description="启用后可使用插件调试、开发者插件路径等高级功能">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="DeveloperBoard" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander Header="Developer mode"
|
||||
Description="Enable developer-focused startup helpers and diagnostics.">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰆬" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding IsDevModeEnabled}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="启用三指滑动"
|
||||
Description="使用三根手指或鼠标右键拖动自由滑动页面,在第一页向右滑动可回到 Windows 桌面(实验性功能)">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Gesture" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander Header="Three-finger desktop swipe"
|
||||
Description="Enable desktop page switching gestures when the current platform supports them.">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰋼" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding EnableThreeFingerSwipe}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="启用融合桌面"
|
||||
Description="允许将组件放置在 Windows 系统桌面上(实验性功能,重启后生效)">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Apps" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander Header="Fused desktop experience"
|
||||
Description="Enable the fused desktop shell and its related experimental entry points.">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰆨" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding EnableFusedDesktop}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<Separator Classes="settings-separator" />
|
||||
|
||||
<ui:SettingsExpander Header="开发者插件路径"
|
||||
Description="指定开发中的插件目录路径,无需打包即可直接加载。多个路径用分号分隔。">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="FolderLink" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpanderItem>
|
||||
<ui:FASettingsExpander Header="Development plugin path"
|
||||
Description="Load a local plugin output directory for iterative debugging without packaging.">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰈼" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<TextBox Text="{Binding DevPluginPath}"
|
||||
Watermark="C:\path\to\plugin\bin\Debug\net10.0"
|
||||
PlaceholderText="C:\path\to\plugin\bin\Debug\net10.0"
|
||||
Width="360"
|
||||
MinWidth="200" />
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpanderItem>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<Separator Classes="settings-separator" />
|
||||
|
||||
<ui:SettingsExpander Header="命令行参数"
|
||||
Description="也可以通过命令行参数或环境变量指定开发者插件路径">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="WindowConsole" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpanderItem>
|
||||
<StackPanel Margin="0,8,0,0" Spacing="8">
|
||||
<TextBlock Text="命令行参数:" FontWeight="SemiBold" />
|
||||
<ui:FASettingsExpander Header="Developer startup arguments"
|
||||
Description="Use these launch arguments or environment variables to start the app in development scenarios.">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󱤰" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<StackPanel Margin="0,8,0,0"
|
||||
Spacing="8">
|
||||
<TextBlock Text="Command-line arguments:"
|
||||
FontWeight="SemiBold" />
|
||||
<Border Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||
CornerRadius="8"
|
||||
Padding="12,8">
|
||||
<TextBlock FontFamily="Cascadia Code, Consolas, monospace"
|
||||
FontSize="12"
|
||||
Text="--dev-plugin <path> 或 -dp <path>"
|
||||
Text="--dev-plugin <path> or -dp <path>"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
<TextBlock Text="环境变量:" FontWeight="SemiBold" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Text="Environment variables:"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,8,0,0" />
|
||||
<Border Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||
CornerRadius="8"
|
||||
Padding="12,8">
|
||||
@@ -87,23 +95,25 @@
|
||||
Text="LMD_DEV_PLUGIN=<path>"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
<TextBlock Text="其他参数:" FontWeight="SemiBold" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Text="Other arguments:"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,8,0,0" />
|
||||
<Border Background="{DynamicResource ControlFillColorDefaultBrush}"
|
||||
CornerRadius="8"
|
||||
Padding="12,8">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock FontFamily="Cascadia Code, Consolas, monospace"
|
||||
FontSize="12"
|
||||
Text="--dev-mode / -dev 启用开发者模式" />
|
||||
Text="--dev-mode / -dev Enable developer mode startup helpers." />
|
||||
<TextBlock FontFamily="Cascadia Code, Consolas, monospace"
|
||||
FontSize="12"
|
||||
Text="--hot-reload / -hr 启用热重载(预留)" />
|
||||
Text="--hot-reload / -hr Enable hot reload for development builds." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
</ui:FASettingsExpanderItem>
|
||||
</ui:FASettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
[SettingsPageInfo(
|
||||
"dev",
|
||||
"开发者",
|
||||
"Developer",
|
||||
SettingsPageCategory.Dev,
|
||||
IconKey = "DeveloperBoard",
|
||||
SortOrder = 0,
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.GeneralSettingsPage"
|
||||
x:DataType="vm:GeneralSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container settings-page-animated">
|
||||
|
||||
<controls:IconText Icon="Globe"
|
||||
Text="{Binding BasicHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<ui:SettingsExpander Header="{Binding LanguageHeader}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Settings" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander Header="{Binding LanguageHeader}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰐄" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ComboBox Width="220"
|
||||
ItemsSource="{Binding Languages}"
|
||||
SelectedItem="{Binding SelectedLanguage}">
|
||||
@@ -27,15 +26,15 @@
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{Binding TimeZoneHeader}"
|
||||
<ui:FASettingsExpander Header="{Binding TimeZoneHeader}"
|
||||
Description="{Binding TimeZoneDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Clock" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰅨" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ComboBox Width="240"
|
||||
ItemsSource="{Binding TimeZones}"
|
||||
SelectedItem="{Binding SelectedTimeZone}">
|
||||
@@ -45,14 +44,14 @@
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{Binding PreviewHeader}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Calendar" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpanderItem>
|
||||
<ui:FASettingsExpander Header="{Binding PreviewHeader}">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󱲀" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="16"
|
||||
RowDefinitions="Auto,Auto"
|
||||
@@ -70,8 +69,8 @@
|
||||
Opacity="0.82"
|
||||
Text="{Binding PreviewDateText}" />
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpanderItem>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<Separator Classes="settings-separator" />
|
||||
|
||||
@@ -79,13 +78,13 @@
|
||||
Text="{Binding RuntimeHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<ui:SettingsExpander Header="{Binding RenderModeHeader}"
|
||||
<ui:FASettingsExpander Header="{Binding RenderModeHeader}"
|
||||
Description="{Binding RuntimeDescription}"
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="DeveloperBoard" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰤈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ComboBox Width="220"
|
||||
ItemsSource="{Binding RenderModes}"
|
||||
SelectedItem="{Binding SelectedRenderMode}">
|
||||
@@ -95,48 +94,47 @@
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
<ui:SettingsExpanderItem>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<TextBlock Text="{Binding RenderModeRestartMessage}"
|
||||
Opacity="0.7"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap" />
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpanderItem>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="淡入淡出效果"
|
||||
<ui:FASettingsExpander Header="Fade startup transition"
|
||||
Description="{Binding FadeTransitionDescription}"
|
||||
IsVisible="{Binding IsSlideTransitionAvailable}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="ArrowUpload" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󱩈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding EnableFadeTransition}"
|
||||
IsEnabled="{Binding IsFadeTransitionToggleEnabled}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="启动滑入滑出效果"
|
||||
Description="启用后,启动和恢复时从屏幕右侧边缘滑入或滑出,仅 Windows 可用。"
|
||||
<ui:FASettingsExpander Header="Slide startup transition"
|
||||
Description="Use a slide-in startup transition on supported Windows builds. This option disables the fade transition."
|
||||
IsVisible="{Binding IsSlideTransitionAvailable}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="ArrowRight" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰣨" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding EnableSlideTransition}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="桌面主窗口在任务栏显示图标"
|
||||
Description="仅控制桌面主窗口在系统任务栏中的图标显示,不影响设置窗口。">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Window" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander Header="Show in taskbar"
|
||||
Description="Keep the main window visible in the taskbar while the desktop host is running.">
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󱯠" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding ShowInTaskbar}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:symbol="using:FluentIcons.Common"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.LauncherSettingsPage"
|
||||
x:DataType="vm:LauncherSettingsPageViewModel">
|
||||
@@ -56,14 +56,14 @@
|
||||
Text="{Binding AppearanceHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
<ui:FASettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding AppearanceHeader}"
|
||||
Description="{Binding AppearanceDescription}"
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="{x:Static symbol:Symbol.Apps}" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpanderItem>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰢈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Text="{Binding ShowTileBackgroundHeader}" />
|
||||
@@ -73,21 +73,21 @@
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
IsChecked="{Binding ShowTileBackground}" />
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpanderItem>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<controls:IconText Icon="Apps"
|
||||
Text="{Binding HiddenHeader}"
|
||||
Margin="0,24,0,4" />
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
<ui:FASettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding HiddenHeader}"
|
||||
Description="{Binding HiddenDescription}"
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Apps" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpanderItem>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󱲀" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpanderItem>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="settings-item-description"
|
||||
IsVisible="{Binding IsHiddenItemsEmpty}"
|
||||
@@ -99,24 +99,24 @@
|
||||
IsVisible="{Binding HasHiddenItems}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:LauncherHiddenItemViewModel">
|
||||
<ui:SettingsExpanderItem Content="{Binding DisplayName}"
|
||||
<ui:FASettingsExpanderItem Content="{Binding DisplayName}"
|
||||
Description="{Binding TypeLabel}"
|
||||
IsClickEnabled="False">
|
||||
<ui:SettingsExpanderItem.IconSource>
|
||||
<fi:SymbolIconSource Symbol="{Binding IconSymbol}" />
|
||||
</ui:SettingsExpanderItem.IconSource>
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󱧼" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpanderItem.IconSource>
|
||||
<ui:FASettingsExpanderItem.Footer>
|
||||
<Button Command="{Binding RestoreCommand}"
|
||||
Content="{Binding RestoreButtonText}"
|
||||
VerticalAlignment="Center" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:FASettingsExpanderItem.Footer>
|
||||
</ui:FASettingsExpanderItem>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpanderItem>
|
||||
</ui:FASettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.NotificationSettingsPage"
|
||||
x:DataType="vm:NotificationSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
@@ -13,15 +13,15 @@
|
||||
Text="{Binding NotificationHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<ui:SettingsExpander Header="{Binding EnableNotificationHeader}"
|
||||
<ui:FASettingsExpander Header="{Binding EnableNotificationHeader}"
|
||||
Description="{Binding EnableNotificationDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Alert" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰀘" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding IsNotificationEnabled}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<Separator Classes="settings-separator" />
|
||||
|
||||
@@ -29,39 +29,39 @@
|
||||
Text="{Binding BehaviorHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<ui:SettingsExpander Header="{Binding HoverPauseHeader}"
|
||||
<ui:FASettingsExpander Header="{Binding HoverPauseHeader}"
|
||||
Description="{Binding HoverPauseDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="CursorHover" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰙬" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding IsHoverPauseEnabled}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{Binding ClickCloseHeader}"
|
||||
<ui:FASettingsExpander Header="{Binding ClickCloseHeader}"
|
||||
Description="{Binding ClickCloseDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="CursorClick" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰅈" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding IsClickCloseEnabled}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Header="{Binding MaxNotificationsHeader}"
|
||||
<ui:FASettingsExpander Header="{Binding MaxNotificationsHeader}"
|
||||
Description="{Binding MaxNotificationsDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="NumberSymbol" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:NumberBox Value="{Binding MaxNotificationsPerPosition}"
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰀐" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<ui:FANumberBox Value="{Binding MaxNotificationsPerPosition}"
|
||||
Minimum="1"
|
||||
Maximum="10"
|
||||
Width="100"
|
||||
SpinButtonPlacementMode="Inline" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
<Separator Classes="settings-separator" />
|
||||
|
||||
@@ -69,12 +69,12 @@
|
||||
Text="{Binding TestHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<ui:SettingsExpander Header="{Binding TestNotificationHeader}"
|
||||
<ui:FASettingsExpander Header="{Binding TestNotificationHeader}"
|
||||
Description="{Binding TestNotificationDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Beaker" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ui:FASettingsExpander.IconSource>
|
||||
<ui:FAFontIconSource Glyph="󰏼" FontFamily="avares://fluenticons.resources.avalonia/Assets#Seagull Fluent Icons" />
|
||||
</ui:FASettingsExpander.IconSource>
|
||||
<ui:FASettingsExpander.Footer>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<ComboBox Width="120"
|
||||
ItemsSource="{Binding TestPositions}"
|
||||
@@ -94,7 +94,7 @@
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<ui:NumberBox Width="100"
|
||||
<ui:FANumberBox Width="100"
|
||||
Minimum="1"
|
||||
Maximum="30"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
@@ -104,8 +104,8 @@
|
||||
<fi:SymbolIcon Symbol="Send" FontSize="16" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</ui:FASettingsExpander.Footer>
|
||||
</ui:FASettingsExpander>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user