合并对设置系统的更新 (#11)

* Add Windows system chrome patchers (Harmony)

Introduce support for toggling the system chrome on Windows using Harmony patchers. Adds Lib.Harmony.Thin to package props and project, new patcher infrastructure (ChromePatchState, PatcherEntrance) and two Harmony patches that disable FluentAvalonia's Windows chrome when configured. Program.cs now loads the chrome setting and installs patchers conditionally on Windows/x86-x64. Settings viewmodel and view updated: expose IsWindowsOs, require restart on appearance changes, migrate SettingsWindow to FAAppWindow and adapt titlebar/layout (include Windows caption placeholder and footer menu items). Also add a .gitkeep and a build log file.

* Refactor settings window UI and theming

Improve theming and layout for the Settings window and related services.

- MaterialSurfaceService: add special material parameters for SettingsWindowBackground (lower alpha, no blur) and avoid hot-switching real backdrops for non-settings windows.
- GlassEffectService: add AdaptiveSettingsWindowTintBrush + ResolveSettingsWindowTintAlpha to provide optional content tinting tied to system material mode.
- SettingsWindowService: refactor theme application into ApplyThemeVariantAndResources, ensure settings window material is applied at show/activate times, and tidy theme/resource application flow.
- SettingsWindow.axaml / .axaml.cs: restructure title bar (separate Grid.Row=0 border) and FANavigationView host, add pane-footer toggle button for :minimal layout, use dynamic corner radius resource, and update toggle/visibility/icon logic and responsive layout code.
- SettingsPages: remove some IconText usages and adjust margins; use DesignCornerRadiusLg for update card corner radius.
- Add NuGet.Config to set local globalPackagesFolder and ignore .nuget/packages in .gitignore.

These changes aim to improve visuals, avoid backdrop overdraw, and make the settings window behavior consistent across themes and layouts.

* Add localization and localize settings pages

Add many new localization keys (en-US and zh-CN) for notifications, developer tools, about page, status bar, and video wallpaper. Update Notification, Dev, About and StatusBar view models to use LocalizationService, expose localized ObservableProperties, and refresh localized text at construction. Localize selection options and test notification texts, and fix notification severity handling. Wire up XAML to the new localized properties (About/Dev/StatusBar pages) and update the settings page title for notifications. Also adjust copyright line generation and replace hardcoded placeholders with bound Watermark properties.

* Redesign settings window with fluent shell & search

Rebuild the settings window as a Fluent shell: adds a custom 48-DIP titlebar with Back, pane toggle, icon/title, search box, restart/more menu, and caption-button spacer; moves compact pane toggle into the titlebar and preserves FANavigationView as the primary navigation surface. Introduces a SettingsSearchService (with UI AutoComplete integration, search indexing, navigation-by-result, and search result highlighting) plus focused tests for search filtering and theme material normalization. Adds navigation history/back stack, updates SettingsViewModels for new bindings and localization keys, and updates General/Apearance pages to expose new strings and options. Implements an "auto" system material mode: default in AppSettingsSnapshot, new MaterialAuto constants and normalization/resolution logic in ThemeAppearanceValues, WindowMaterialService and MaterialSurfaceService adjustments to prefer Mica on Win11 and Acrylic on Win10 using TransparencyLevelHint. GlassEffectService and AppearanceThemeService updated to use effective material mode and to track live theme state changes. Adds localization entries (en-US, zh-CN), spec/tasks docs, and other UI/style tweaks to support the redesign.

* fix.修折叠与展开按钮

* Add OOBE startup presentation and settings merge

Introduce a new OOBE step for "Startup & Presentation" that exposes startup and UI preferences in OobeWindow (toggles for taskbar, slide/fade transitions, fused popup, and autostart). Add HostAppSettingsOobeMerger to read/write Host settings.json (PascalCase fields) and MergeStartupPresentation behavior, plus LauncherWindowsStartupService to sync the current Launcher into the Windows Run key on Windows. Wire UI handlers, persist choices on Next, and load defaults when entering the step. Include unit tests for the merger, adjust SettingsWindow navigation pane/toggle handling, and update docs/LAUNCHER.md to describe the new OOBE step and implementation files.

* Move whiteboard persistence to file storage

Switch whiteboard note storage from legacy DB rows to per-note JSON files and add migration support. Update WhiteboardNoteSnapshot schema (version bump, viewport, canvas, expires, PathSvgData) and change IWhiteboardNotePersistenceService.SaveNote to return bool to surface write failures (e.g. read-only files). Implement file-based WhiteboardNotePersistenceService with legacy DB migration/cleanup, retention handling, and logging. Add comprehensive unit tests for persistence, stroke path builder, SVG import and viewport helper. Also add ThirdParty/DotNetCampus.InkCanvas project and reference it in the main csproj, and bump PostHog package to 2.6.0.

* Introduce render gate and chart caching

Replace UI DispatcherTimer polling with a StudySnapshotRenderGate across multiple widgets to queue and apply only the latest analytics snapshot; components updated include StudyDeductionReasonsWidget, StudyEnvironmentWidget, StudyInterruptDensityWidget, StudyNoiseCurveWidget. Add StudySnapshotRenderGate implementation to coordinate rendering and monitoring leases and update subscription/lease lifecycle handling (subscribe/unsubscribe, Acquire/Dispose leases, Clear/Dispose gate). Rewrite chart controls (StudyNoiseCurveChartControl and StudyNoiseDistributionScatterChartControl) to use stable logical-time origins, split series into static vs dynamic tails, add geometry/sample caching, stable jitter/coordinate mapping helpers, and expose internal helpers & counts for testing. Add unit tests (StudyComponentRenderingTests) covering the render gate and chart behaviors (layer counts, logical X mapping, stable jitter, cache rebuild). These changes improve rendering correctness and performance by avoiding redundant renders and enabling deterministic chart layout.

* Use MaterialColorSnapshot in appearance flow

Introduce unified material/color spec and tests, and refactor appearance plumbing to use MaterialColorSnapshot as the single source of truth. Add .trae material-color-service spec/checklist/tasks and integration/unit tests for plugin mapping and appearance VM behavior. AppearanceChangedEvent extended with new appearance change flags and HasChanged logic. ComponentEditorMaterialThemeAdapter rewritten to accept MaterialColorSnapshot and derive palette from snapshot data. Simplify AppearanceSettingsPageViewModel and related view code: remove legacy preview/custom-seed UI logic, preserve material/color fields when updating theme or corner radius, and update save calls to use with-expressions. Update ComponentEditorWindow to use adapter-provided OnPrimary brush and minor docs updates.

* Add material color services, plugin DTOs, and tests

Introduce IPC wire-format appearance DTOs (PluginIsolation.Contracts) and clarify they are distinct from the runtime PluginSdk snapshot. Update PluginSdk comments to document the runtime-facing snapshot shape. Change ComponentColorSchemeHelper to use the HostMaterialColorProvider and add an overload that accepts a MaterialColorSnapshot. Add new services and pipelines (MaterialColorService, MaterialSurfaceService, WindowMaterialService, WallpaperColorPipeline) and refactor AppearanceThemeService to depend on MaterialColorService while removing legacy internal implementations. Add multiple unit tests (ComponentColorSchemeHelper, PluginAppearanceBoundary, SettingsCatalogService, WallpaperSettingsPageViewModel) and update localization resources with new material_color and wallpaper keys.

* Add CODE_WIKI and update localization

Add a comprehensive CODE_WIKI.md documenting project architecture, modules, startup flow, plugin system, testing and developer workflows. Update localization resources (en-US.json, zh-CN.json) with new/translated keys for wallpaper controls (custom color UI), material & color settings (semantic roles, surfaces, refresh/polling state), appearance (corner radius), status bar font size options, privacy policy text, component library labels, clock settings, and new language entry (Korean). Also modify settings-related ViewModels and Settings page views to surface these new features and texts (MaterialColorSettingsPageViewModel.cs, SettingsViewModels.cs, WallpaperSettingsPageViewModel.cs, MainWindow.SettingsHardCut.Stubs.cs, ComponentsSettingsPage.axaml, WallpaperSettingsPage.axaml).

* Add Data settings page and storage scanner

Introduce a new "Data" settings page to visualize and manage local app storage. Adds DataStorageService (scanning, disk info, clean operations), DataSettingsPageViewModel, XAML view and code-behind, and HexToColor/HexToBrush converters; registers converters in App.axaml. Also update localization strings for the new page and add icon mapping so the settings entry uses the Database icon. Enables per-category and global cleaning workflows and formatted size display.

* Add IPC backoff/retries and safer disposal

Introduce exponential backoff, jitter and retry logic across IPC components to improve robustness and avoid tight retry loops; make disposal idempotent and add connection guards. Key changes:
- LauncherCoordinatorIpcServer / LauncherIpcServer: add backoff constants, ComputeBackoff(), consecutive error tracking and delayed retries with jitter.
- LanMountainDesktopIpcClient / LauncherIpcClient: add connect retry loops, timeouts, delayed retries, improved error logging, and use ArrayPool for buffered async writes; ensure proper cleanup on failures.
- PublicIpcHostService: add disposed flag, guard OnPeerConnected and Dispose, and clear connected peers on dispose.
- Add many auto-generated commit analysis docs under docs/auto_commit_md and new scripts for analyzing/generating commit docs.
These changes aim to make IPC connection handling more resilient and resource-safe.

* Add preview controls and settings UI tweaks

Introduce GridPreviewControl and CornerRadiusPreviewControl for visual previews and wire them into the Components settings (add ScreenAspectRatio, CornerRadiusPreviewValue, and screen aspect init). Refactor ComponentsSettingsPage UI to show live previews. Improve DataSettingsPage layout and storage bar logic (use item percentages directly, include remaining segment, adjust visuals and visibility triggers). Simplify LauncherSettingsPage header/appearance layout. Add SECURITY_AUDIT_REPORT.md, analysis summary, mockup HTML, and a local .claude settings file.

* Add install checkpoint/resume and DDSS workflows

Introduce install checkpoint support and resume logic for updates, plus related locking and validation. Adds InstallCheckpoint model, AppJsonContext serialization, and UpdatePaths helpers for deployment lock, apply-in-progress lock and install-checkpoint path. UpdateEngineService gains checkpoint load/save/delete, incoming-state validation, resume logic for PLONDS and legacy updates, apply lock handling, and safer cleanup; ApplyPendingPlondsUpdateAsync and ApplyPendingUpdate flow updated accordingly. Add DeploymentLock contract and extend UpdateState with pause/resume/cancel helpers. Tests updated to cover stale/valid checkpoint resume and legacy/PLONDS flows. CI: enhance ddss-publish to detect release channel, validate S3 assets, prepare and atomically publish channel pointer; add ddss-rollback workflow to publish rollbacks; adjust plonds-build concurrency and release events.

* changed.更了好多

* fix.消息盒子媒体播放器组件服务修复

* change.重做天气,为回到系统提供自定义功能。

* feat.airapp与融合桌面

* feat.动画优化与更新界面

* feat.数字时钟,白板功能修复

* feat.完善了时钟轻应用,为启动器提供了多语言支持

* feat.发布与打包优化

* changed.天气选项卡更新
This commit is contained in:
lincube
2026-05-19 07:55:21 +08:00
committed by GitHub
parent 458494d131
commit 7a70476ce8
904 changed files with 78052 additions and 18366 deletions

View File

@@ -0,0 +1,7 @@
# Checklist
- [x] Main app builds in Debug.
- [x] AirAppHost builds in Debug.
- [x] Tests project builds in Debug.
- [x] `AirAppLauncherServiceTests` pass.
- [ ] Manual UI verification on a running desktop session.

View File

@@ -0,0 +1,26 @@
# Air APP Whiteboard
## Goal
Allow the built-in whiteboard desktop components to open a full-screen Air APP that runs in `LanMountainDesktop.AirAppHost` and reuses the same persisted whiteboard note as the source component instance.
## Scope
- Add a toolbar surface-mode button to `WhiteboardWidget`.
- In component mode, the button opens the `whiteboard` Air APP through `IAirAppLauncherService`.
- In Air APP mode, the same button saves the current note and closes the Air APP window.
- `DesktopWhiteboard` and `DesktopBlackboardLandscape` share the same mechanism and keep using their component id plus placement id as the note identity.
- `LanMountainDesktop.AirAppHost` may reference the host assembly to reuse built-in UI controls, but the host app must not reference AirAppHost as a normal assembly dependency.
## Out of Scope
- Third-party Air APP SDK declarations.
- Whiteboard feature rewrites or alternate whiteboard persistence.
- Taskbar minimization behavior; v1 closes the Air APP window when the user exits from the bottom toolbar.
## Acceptance
- Building the main app also builds and copies `LanMountainDesktop.AirAppHost` output.
- Clicking the whiteboard toolbar full-screen button launches a separate AirAppHost process.
- Repeated opens of the same whiteboard component instance activate the existing process instead of spawning duplicates.
- Closing and reopening the Air APP keeps the same whiteboard contents.

View File

@@ -0,0 +1,8 @@
# Tasks
- [x] Add `whiteboard` launch support to `AirAppLauncherService`.
- [x] Add whiteboard single-instance keys based on component id and placement id.
- [x] Add component/Air APP surface modes to `WhiteboardWidget`.
- [x] Render `WhiteboardWidget` full screen from `LanMountainDesktop.AirAppHost`.
- [x] Keep AirAppHost build/copy output available from the main app build.
- [x] Add launcher argument and instance-key tests.

View File

@@ -0,0 +1,8 @@
# Checklist
- [x] Descriptor supports Standard, Borderless, FullScreen, Tool, and BackgroundOnly modes.
- [x] World Clock Air APP uses FluentAvalonia standard title-bar chrome.
- [x] Whiteboard Air APP opens as a fullscreen titlebar-less window.
- [x] Air APP windows do not use fused desktop bottom-most services.
- [x] Air APP windows do not use `Topmost=true` promotion.
- [ ] Manual verification for each chrome mode once non-built-in Air APP declarations are added.

View File

@@ -0,0 +1,22 @@
# Air APP Window Chrome
## Goal
Give Air APPs explicit window chrome modes so title bars, fullscreen windows, borderless windows, tool windows, and future background-only apps are configured by the Air APP host instead of ad hoc component code.
## Behavior
- Air APP host resolves an `AirAppWindowDescriptor` from launch options before creating content.
- Supported chrome modes are `Standard`, `Borderless`, `FullScreen`, `Tool`, and `BackgroundOnly`.
- `Standard` uses FluentAvalonia `FAAppWindow` title-bar chrome and normal app-window behavior.
- `Borderless` removes title-bar chrome while keeping a normal app window surface.
- `FullScreen` removes title-bar chrome and enters fullscreen.
- `Tool` keeps FluentAvalonia title-bar chrome but disables resizing and hides the taskbar entry.
- `BackgroundOnly` is reserved for a later background Air APP lifecycle and is not used by built-in v1 apps.
- Built-in `world-clock` uses `Standard`; built-in `whiteboard` uses `FullScreen`.
## Out of Scope
- Third-party plugin Air APP declarations.
- Replacing Launcher lifecycle IPC.
- Moving title-bar rendering into desktop components.

View File

@@ -0,0 +1,8 @@
# Tasks
- [x] Add `AirAppWindowChromeMode` and `AirAppWindowDescriptor`.
- [x] Map built-in `world-clock` to `Standard` chrome.
- [x] Map built-in `whiteboard` to `FullScreen` chrome.
- [x] Apply descriptor settings from `AirAppWindow`.
- [x] Add regression tests for supported modes and built-in mode mapping.
- [x] Replace the hand-rolled Air APP title bar with FluentAvalonia `FAAppWindow` chrome.

View File

@@ -0,0 +1,13 @@
# Checklist
- [x] Clicking `DesktopClock` and `DesktopWorldClock` opens the same global Clock Air APP type.
- [x] Repeated `world-clock` open requests use the global `world-clock:clock-suite:global` instance key.
- [x] Whiteboard Air APP keeps its per-component instance key behavior.
- [x] Clock Air APP opens as a normal application window, not a desktop-layer window.
- [x] Clock Air APP settings are independent from desktop clock widget settings.
- [x] Corrupt Clock Air APP settings fall back to defaults.
- [x] World clock time labels support 12-hour, 24-hour, and follow-system formatting.
- [x] Added localization keys are present in all four supported language files.
- [x] Build and automated tests pass.
- [ ] Manual visual verification in all four languages.
- [ ] Manual verification that minimizing keeps stopwatch and timer running while closing stops them.

View File

@@ -0,0 +1,42 @@
# Clock Air APP MVP
## Goal
Upgrade the built-in `world-clock` Air APP into a focused clock suite while keeping desktop clock widgets as lightweight launch entry points.
## Scope
- Keep the existing Air APP id `world-clock` for Launcher lifecycle compatibility.
- Use one global Clock Air APP instance for every clock widget entry point.
- Provide four tabs: World Clock, Stopwatch, Timer, and Settings.
- Store Clock Air APP settings independently from desktop widget settings at `AirApps/Clock/settings.json`.
- Follow the host language setting and provide localized text for `zh-CN`, `en-US`, `ja-JP`, and `ko-KR`.
## Behavior
- `world-clock` opens as a standard resizable FluentAvalonia window.
- The default window size is approximately `780x560`, with a minimum of `680x480`.
- World Clock shows local time and a configurable city list.
- Default city list is Beijing, London, Sydney, and New York.
- Users can add, remove, and reorder city entries during the Air APP session; the list persists across restarts.
- Stopwatch supports start, pause, resume, lap, and reset; laps are kept in the current window session, up to 50 entries.
- Timer supports fixed presets, a custom minute duration, start, pause, resume, reset, and a completed state.
- Closing the Clock Air APP stops stopwatch and timer activity.
- Minimizing the window keeps stopwatch and timer activity running.
- Timer completion can activate the Clock Air APP window when the setting is enabled.
## Settings
- Time format: follow system, 24-hour, or 12-hour.
- Show seconds.
- Startup tab: last used tab, World Clock, Stopwatch, or Timer.
- Activate window when timer finishes.
## Out of Scope
- Desktop clock widget visual redesign.
- Alarms.
- Focus mode.
- System notifications.
- Running stopwatch or timer after the Air APP window is closed.
- Third-party plugin Air APP declarations.

View File

@@ -0,0 +1,15 @@
# Tasks
- [x] Add Clock Air APP settings snapshot and JSON store.
- [x] Add shared Clock Air APP time formatting helpers.
- [x] Add stopwatch and timer state models with focused tests.
- [x] Replace the old world-clock view with `ClockAirAppView`.
- [x] Configure `world-clock` as a standard resizable Air APP window.
- [x] Make `world-clock` use a global single-instance key independent of source component id.
- [x] Add world clock city add, remove, and reorder behavior.
- [x] Add stopwatch tab with lap support.
- [x] Add timer tab with presets and custom duration.
- [x] Add independent Clock Air APP settings tab.
- [x] Add `zh-CN`, `en-US`, `ja-JP`, and `ko-KR` localization keys.
- [x] Ensure AirAppHost output includes localization JSON resources.
- [x] Add regression tests for Launcher keying, descriptors, settings, formatting, stopwatch, timer, and localization coverage.

View File

@@ -0,0 +1,104 @@
# 数据设置页设计文档
## 概述
在设置窗口中新增「数据」设置页,用于可视化展示和管理阑山桌面产生的各类本地数据。采用 Fluent Design 风格的横向堆叠条形图展示存储分布。
## 设计目标
1. 让用户直观了解阑山桌面占用的存储空间
2. 提供各类数据的占比可视化
3. 支持按类别清理数据
4. 显示相对于磁盘总容量的占比
## 页面结构
### 存储概览区域
顶部一个卡片,包含:
- **横向堆叠条形图** — 各类数据用不同颜色的分段表示
- **总占用大小** — 阑山桌面数据总大小(如 "1.2 GB"
- **磁盘占比** — 占总磁盘空间的百分比(如 "占 C 盘 0.5%"
- **图例** — 各颜色对应的数据类型
### 数据类型详情列表
下方列表展示每类数据:
- 图标 + 名称
- 占用大小
- 描述/路径提示
- 「清理」按钮(如适用)
### 操作按钮
- 「刷新」— 重新扫描数据大小
- 「一键清理」— 清理所有可清理的数据
## 数据类型
| 类型 | 颜色 | 可清理 | 路径 |
|------|------|--------|------|
| 日志文件 | 灰色 | 是 | `log/` |
| 白板笔记 | 橙色 | 是(过期) | `Whiteboards/` |
| 插件数据 | 蓝色 | 是 | `Extensions/Plugins/` |
| 插件市场缓存 | 紫色 | 是 | `PluginMarket/` |
| 壁纸文件 | 粉色 | 是 | `Wallpapers/` |
| 设置文件 | 绿色 | 否 | `settings.json` |
## 技术实现
### 新增文件
- `LanMountainDesktop/Views/SettingsPages/DataSettingsPage.axaml` — 页面视图
- `LanMountainDesktop/Views/SettingsPages/DataSettingsPage.axaml.cs` — 页面代码隐藏
- `LanMountainDesktop/ViewModels/DataSettingsPageViewModel.cs` — 视图模型
- `LanMountainDesktop/Services/DataStorageService.cs` — 数据扫描服务
### 修改文件
- `LanMountainDesktop/Views/SettingsWindow.axaml.cs` — 图标映射MapIcon添加 Database 图标
### 设置页注册
```csharp
[SettingsPageInfo(
"data",
"Data",
SettingsPageCategory.General,
IconKey = "Database",
SortOrder = 5,
TitleLocalizationKey = "settings.data.title",
DescriptionLocalizationKey = "settings.data.description")]
```
## 视觉设计
### 堆叠条形图
- 高度24-32dp
- 圆角:使用 `DesignCornerRadiusSm`
- 分段间距2dp
- 未占用空间:透明或浅色背景
### 颜色方案
使用 Material Design 颜色,与主题协调:
- 日志Gray / BlueGray
- 白板Orange / Amber
- 插件Blue / Indigo
- 缓存Purple / DeepPurple
- 壁纸Pink
- 设置Green / Teal
## 交互行为
1. 页面加载时自动扫描数据大小(异步)
2. 显示加载指示器
3. 清理操作需要确认对话框
4. 清理完成后自动刷新数据
## 安全考虑
- 清理前确认用户意图
- 设置文件不可清理(防止误删配置)
- 清理操作记录日志

View File

@@ -0,0 +1,777 @@
# 数据设置页实现计划
> **Goal:** 在设置窗口中新增「数据」设置页,可视化展示阑山桌面各类本地数据的存储占用,支持数据清理。
> **Architecture:** 采用 MVVM 模式,新增 DataStorageService 负责异步扫描各类数据大小DataSettingsPage 使用 Fluent Design 横向堆叠条形图展示存储分布。
> **Tech Stack:** Avalonia UI, FluentAvaloniaUI, CommunityToolkit.Mvvm, C# 13
---
## 文件结构
| 文件 | 职责 |
|------|------|
| `LanMountainDesktop/Services/DataStorageService.cs` | 扫描各类数据目录大小,计算磁盘总容量 |
| `LanMountainDesktop/ViewModels/DataSettingsPageViewModel.cs` | 数据设置页视图模型,绑定存储数据和清理命令 |
| `LanMountainDesktop/Views/SettingsPages/DataSettingsPage.axaml` | 数据设置页 XAML 视图(堆叠条形图 + 列表) |
| `LanMountainDesktop/Views/SettingsPages/DataSettingsPage.axaml.cs` | 页面代码隐藏,注册设置页属性 |
| `LanMountainDesktop/Views/SettingsWindow.axaml.cs` | 修改图标映射,添加 Database 图标 |
---
## Task 1: 创建 DataStorageService
**Files:**
- Create: `LanMountainDesktop/Services/DataStorageService.cs`
**职责:** 扫描阑山桌面各类数据的存储占用,计算磁盘总容量。
- [ ] **Step 1: 创建 DataStorageService**
```csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace LanMountainDesktop.Services;
public sealed record StorageCategoryInfo(
string Id,
string Name,
string Description,
string DirectoryPath,
bool IsCleanable,
string ColorHex);
public sealed record StorageScanResult(
StorageCategoryInfo Category,
long SizeBytes,
double PercentageOfTotal);
public sealed class DataStorageService
{
private static readonly IReadOnlyList<StorageCategoryInfo> Categories = new List<StorageCategoryInfo>
{
new("logs", "日志文件", "应用运行日志", "", true, "#9E9E9E"),
new("whiteboards", "白板笔记", "桌面白板笔记数据", "", true, "#FF9800"),
new("plugins", "插件数据", "已安装插件文件", "", true, "#2196F3"),
new("market", "插件市场缓存", "插件市场元数据缓存", "", true, "#9C27B0"),
new("wallpapers", "壁纸文件", "下载的壁纸资源", "", true, "#E91E63"),
new("settings", "设置文件", "应用配置数据", "", false, "#4CAF50")
};
public IReadOnlyList<StorageCategoryInfo> GetCategories() => Categories;
public async Task<IReadOnlyList<StorageScanResult>> ScanAsync(CancellationToken cancellationToken = default)
{
var results = new List<StorageScanResult>();
var dataRoot = AppDataPathProvider.GetDataRoot();
var logDirectory = AppLogger.LogDirectory;
long totalSize = 0;
var categorySizes = new Dictionary<string, long>();
foreach (var category in Categories)
{
cancellationToken.ThrowIfCancellationRequested();
string path = category.Id switch
{
"logs" => logDirectory,
"settings" => dataRoot,
_ => Path.Combine(dataRoot, category.DirectoryPath)
};
long size = 0;
if (category.Id == "settings")
{
size = await GetSettingsSizeAsync(dataRoot, cancellationToken);
}
else if (Directory.Exists(path))
{
size = await GetDirectorySizeAsync(path, cancellationToken);
}
categorySizes[category.Id] = size;
totalSize += size;
}
foreach (var category in Categories)
{
var size = categorySizes.GetValueOrDefault(category.Id, 0);
var percentage = totalSize > 0 ? (double)size / totalSize * 100 : 0;
results.Add(new StorageScanResult(category, size, percentage));
}
return results;
}
public async Task<long> GetTotalDiskSpaceAsync(CancellationToken cancellationToken = default)
{
return await Task.Run(() =>
{
var dataRoot = AppDataPathProvider.GetDataRoot();
var driveInfo = new DriveInfo(Path.GetPathRoot(dataRoot) ?? dataRoot);
return driveInfo.TotalSize;
}, cancellationToken);
}
public async Task<long> GetAvailableDiskSpaceAsync(CancellationToken cancellationToken = default)
{
return await Task.Run(() =>
{
var dataRoot = AppDataPathProvider.GetDataRoot();
var driveInfo = new DriveInfo(Path.GetPathRoot(dataRoot) ?? dataRoot);
return driveInfo.AvailableFreeSpace;
}, cancellationToken);
}
public async Task<bool> CleanCategoryAsync(string categoryId, CancellationToken cancellationToken = default)
{
var category = Categories.FirstOrDefault(c =>
string.Equals(c.Id, categoryId, StringComparison.OrdinalIgnoreCase));
if (category is null || !category.IsCleanable)
{
return false;
}
var dataRoot = AppDataPathProvider.GetDataRoot();
string path = categoryId switch
{
"logs" => AppLogger.LogDirectory,
_ => Path.Combine(dataRoot, category.DirectoryPath)
};
if (!Directory.Exists(path))
{
return false;
}
return await Task.Run(() =>
{
try
{
if (categoryId == "logs")
{
foreach (var file in Directory.GetFiles(path, "*.log"))
{
cancellationToken.ThrowIfCancellationRequested();
TryDeleteFile(file);
}
}
else
{
foreach (var file in Directory.GetFiles(path, "*", SearchOption.AllDirectories))
{
cancellationToken.ThrowIfCancellationRequested();
TryDeleteFile(file);
}
foreach (var dir in Directory.GetDirectories(path, "*", SearchOption.AllDirectories)
.OrderByDescending(d => d.Length))
{
cancellationToken.ThrowIfCancellationRequested();
TryDeleteDirectory(dir);
}
}
AppLogger.Info("DataStorage", $"Cleaned category '{categoryId}' at '{path}'.");
return true;
}
catch (Exception ex)
{
AppLogger.Warn("DataStorage", $"Failed to clean category '{categoryId}'.", ex);
return false;
}
}, cancellationToken);
}
private static async Task<long> GetDirectorySizeAsync(string path, CancellationToken cancellationToken)
{
return await Task.Run(() =>
{
long size = 0;
try
{
foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var info = new FileInfo(file);
if (info.Exists)
{
size += info.Length;
}
}
catch
{
// Ignore files we can't access
}
}
}
catch
{
// Ignore directories we can't access
}
return size;
}, cancellationToken);
}
private static async Task<long> GetSettingsSizeAsync(string dataRoot, CancellationToken cancellationToken)
{
return await Task.Run(() =>
{
long size = 0;
var settingFiles = new[] { "settings.json", "plugin-settings.json", "launcher-settings.json" };
foreach (var file in settingFiles)
{
cancellationToken.ThrowIfCancellationRequested();
var path = Path.Combine(dataRoot, file);
if (File.Exists(path))
{
try
{
size += new FileInfo(path).Length;
}
catch
{
// Ignore
}
}
}
return size;
}, cancellationToken);
}
private static void TryDeleteFile(string path)
{
try
{
File.SetAttributes(path, FileAttributes.Normal);
File.Delete(path);
}
catch
{
// Ignore deletion failures
}
}
private static void TryDeleteDirectory(string path)
{
try
{
Directory.Delete(path, false);
}
catch
{
// Ignore deletion failures
}
}
public static string FormatBytes(long bytes)
{
const long KB = 1024;
const long MB = KB * 1024;
const long GB = MB * 1024;
const long TB = GB * 1024;
return bytes switch
{
>= TB => $"{bytes / (double)TB:F2} TB",
>= GB => $"{bytes / (double)GB:F2} GB",
>= MB => $"{bytes / (double)MB:F2} MB",
>= KB => $"{bytes / (double)KB:F2} KB",
_ => $"{bytes} B"
};
}
}
```
---
## Task 2: 创建 DataSettingsPageViewModel
**Files:**
- Create: `LanMountainDesktop/ViewModels/DataSettingsPageViewModel.cs`
- [ ] **Step 1: 创建 ViewModel**
```csharp
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.ViewModels;
public sealed partial class DataStorageItemViewModel : ObservableObject
{
public string Id { get; }
public string Name { get; }
public string Description { get; }
public string ColorHex { get; }
public bool IsCleanable { get; }
[ObservableProperty]
private string _sizeText = "--";
[ObservableProperty]
private double _percentage;
[ObservableProperty]
private bool _isCleaning;
public DataStorageItemViewModel(StorageCategoryInfo category)
{
Id = category.Id;
Name = category.Name;
Description = category.Description;
ColorHex = category.ColorHex;
IsCleanable = category.IsCleanable;
}
public void UpdateSize(long sizeBytes, double percentage)
{
SizeText = DataStorageService.FormatBytes(sizeBytes);
Percentage = percentage;
}
}
public sealed partial class DataSettingsPageViewModel : ViewModelBase
{
private readonly DataStorageService _storageService = new();
private CancellationTokenSource? _scanCts;
[ObservableProperty]
private string _pageTitle = "数据与存储";
[ObservableProperty]
private string _totalSizeText = "--";
[ObservableProperty]
private string _diskUsageText = "--";
[ObservableProperty]
private double _diskUsagePercentage;
[ObservableProperty]
private bool _isScanning;
[ObservableProperty]
private bool _hasData;
public ObservableCollection<DataStorageItemViewModel> Items { get; } = new();
public DataSettingsPageViewModel()
{
var categories = _storageService.GetCategories();
foreach (var category in categories)
{
Items.Add(new DataStorageItemViewModel(category));
}
_ = ScanAsync();
}
[RelayCommand]
private async Task ScanAsync()
{
_scanCts?.Cancel();
_scanCts = new CancellationTokenSource();
var token = _scanCts.Token;
IsScanning = true;
try
{
var results = await _storageService.ScanAsync(token);
var totalSize = results.Sum(r => r.SizeBytes);
var totalDisk = await _storageService.GetTotalDiskSpaceAsync(token);
await Dispatcher.UIThread.InvokeAsync(() =>
{
TotalSizeText = DataStorageService.FormatBytes(totalSize);
DiskUsagePercentage = totalDisk > 0 ? (double)totalSize / totalDisk * 100 : 0;
DiskUsageText = $"占总磁盘 {DiskUsagePercentage:F1}%";
HasData = totalSize > 0;
foreach (var result in results)
{
var item = Items.FirstOrDefault(i =>
string.Equals(i.Id, result.Category.Id, StringComparison.OrdinalIgnoreCase));
item?.UpdateSize(result.SizeBytes, result.PercentageOfTotal);
}
});
}
catch (OperationCanceledException)
{
// Ignore cancellation
}
catch (Exception ex)
{
AppLogger.Warn("DataSettings", "Failed to scan storage.", ex);
}
finally
{
IsScanning = false;
}
}
[RelayCommand]
private async Task CleanAsync(string categoryId)
{
var item = Items.FirstOrDefault(i =>
string.Equals(i.Id, categoryId, StringComparison.OrdinalIgnoreCase));
if (item is null || !item.IsCleanable)
{
return;
}
item.IsCleaning = true;
try
{
await _storageService.CleanCategoryAsync(categoryId);
await ScanAsync();
}
catch (Exception ex)
{
AppLogger.Warn("DataSettings", $"Failed to clean category '{categoryId}'.", ex);
}
finally
{
item.IsCleaning = false;
}
}
[RelayCommand]
private async Task CleanAllAsync()
{
foreach (var item in Items.Where(i => i.IsCleanable))
{
item.IsCleaning = true;
}
try
{
foreach (var item in Items.Where(i => i.IsCleanable))
{
await _storageService.CleanCategoryAsync(item.Id);
}
await ScanAsync();
}
catch (Exception ex)
{
AppLogger.Warn("DataSettings", "Failed to clean all categories.", ex);
}
finally
{
foreach (var item in Items)
{
item.IsCleaning = false;
}
}
}
}
```
---
## Task 3: 创建 DataSettingsPage.axaml
**Files:**
- Create: `LanMountainDesktop/Views/SettingsPages/DataSettingsPage.axaml`
- [ ] **Step 1: 创建 XAML 视图**
```xml
<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"
x:Class="LanMountainDesktop.Views.SettingsPages.DataSettingsPage"
x:DataType="vm:DataSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container settings-page-animated"
Spacing="16">
<!-- 存储概览卡片 -->
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="20">
<StackPanel Spacing="12">
<TextBlock Text="存储概览"
FontSize="16"
FontWeight="SemiBold" />
<!-- 堆叠条形图 -->
<Grid Height="28"
IsVisible="{Binding HasData}">
<Border Background="{DynamicResource ControlFillColorTertiaryBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
ClipToBounds="True">
<StackPanel Orientation="Horizontal"
x:Name="StorageBarPanel">
<!-- 动态生成分段 -->
</StackPanel>
</Border>
</Grid>
<!-- 总大小和磁盘占比 -->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Spacing="8">
<TextBlock Text="{Binding TotalSizeText}"
FontSize="24"
FontWeight="SemiBold" />
<TextBlock Text="{Binding DiskUsageText}"
VerticalAlignment="Bottom"
Margin="0,0,0,4"
Opacity="0.7" />
</StackPanel>
<Button Grid.Column="1"
Command="{Binding ScanCommand}"
IsEnabled="{Binding !IsScanning}"
VerticalAlignment="Center">
<StackPanel Orientation="Horizontal"
Spacing="6">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular"
FontSize="14" />
<TextBlock Text="刷新" />
</StackPanel>
</Button>
</Grid>
<!-- 图例 -->
<ItemsControl ItemsSource="{Binding Items}"
IsVisible="{Binding HasData}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"
ItemWidth="140"
ItemHeight="28" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:DataStorageItemViewModel">
<StackPanel Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center">
<Border Width="12"
Height="12"
CornerRadius="2"
Background="{Binding ColorHex, Converter={StaticResource HexToBrushConverter}}" />
<TextBlock Text="{Binding Name}"
FontSize="12"
Opacity="0.8" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<!-- 数据类型详情列表 -->
<TextBlock Text="数据详情"
FontSize="16"
FontWeight="SemiBold"
Margin="0,8,0,0" />
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:DataStorageItemViewModel">
<Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}"
Padding="16"
Margin="0,4">
<Grid ColumnDefinitions="Auto,*,Auto,Auto"
ColumnSpacing="12">
<Border Grid.Column="0"
Width="12"
Height="12"
CornerRadius="2"
Background="{Binding ColorHex, Converter={StaticResource HexToBrushConverter}}"
VerticalAlignment="Center" />
<StackPanel Grid.Column="1"
VerticalAlignment="Center">
<TextBlock Text="{Binding Name}"
FontWeight="SemiBold" />
<TextBlock Text="{Binding Description}"
FontSize="12"
Opacity="0.6" />
</StackPanel>
<TextBlock Grid.Column="2"
Text="{Binding SizeText}"
VerticalAlignment="Center"
FontWeight="SemiBold"
Opacity="0.8" />
<Button Grid.Column="3"
Command="{Binding $parent[ItemsControl].((vm:DataSettingsPageViewModel)DataContext).CleanCommand}"
CommandParameter="{Binding Id}"
IsVisible="{Binding IsCleanable}"
IsEnabled="{Binding !IsCleaning}"
VerticalAlignment="Center">
<StackPanel Orientation="Horizontal"
Spacing="4">
<fi:FluentIcon Icon="Delete"
IconVariant="Regular"
FontSize="14" />
<TextBlock Text="清理" />
</StackPanel>
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- 一键清理 -->
<Button Command="{Binding CleanAllCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Margin="0,8">
<StackPanel Orientation="Horizontal"
Spacing="6">
<fi:FluentIcon Icon="Broom"
IconVariant="Regular"
FontSize="16" />
<TextBlock Text="一键清理所有可清理数据" />
</StackPanel>
</Button>
</StackPanel>
</ScrollViewer>
</UserControl>
```
---
## Task 4: 创建 DataSettingsPage.axaml.cs
**Files:**
- Create: `LanMountainDesktop/Views/SettingsPages/DataSettingsPage.axaml.cs`
- [ ] **Step 1: 创建代码隐藏**
```csharp
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.ViewModels;
namespace LanMountainDesktop.Views.SettingsPages;
[SettingsPageInfo(
"data",
"Data",
SettingsPageCategory.General,
IconKey = "Database",
SortOrder = 5,
TitleLocalizationKey = "settings.data.title",
DescriptionLocalizationKey = "settings.data.description")]
public partial class DataSettingsPage : SettingsPageBase
{
public DataSettingsPage()
: this(new DataSettingsPageViewModel())
{
}
public DataSettingsPage(DataSettingsPageViewModel viewModel)
{
ViewModel = viewModel;
DataContext = ViewModel;
InitializeComponent();
}
public DataSettingsPageViewModel ViewModel { get; }
}
```
---
## Task 5: 修改 SettingsWindow.axaml.cs 添加图标映射
**Files:**
- Modify: `LanMountainDesktop/Views/SettingsWindow.axaml.cs`
- [ ] **Step 1: 在 MapIcon 方法中添加 Database 图标映射**
`MapIcon` 方法的 switch 表达式中添加:
```csharp
"Database" => Symbol.Database,
```
---
## Task 6: 添加颜色转换器(如需要)
**Files:**
- Modify: `LanMountainDesktop/Theme/``LanMountainDesktop/Controls/` 中的资源字典
如果项目中没有 HexToBrushConverter需要创建一个简单的值转换器
```csharp
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace LanMountainDesktop.Converters;
public class HexToBrushConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is string hex && !string.IsNullOrWhiteSpace(hex))
{
try
{
return new SolidColorBrush(Color.Parse(hex));
}
catch
{
// Ignore parse errors
}
}
return new SolidColorBrush(Colors.Gray);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
```
---
## 测试验证
1. 构建项目:`dotnet build LanMountainDesktop.slnx -c Debug`
2. 运行应用:`dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj`
3. 打开设置窗口,确认「数据」选项卡出现在左侧导航中
4. 点击「数据」选项卡,确认:
- 堆叠条形图显示各类数据占比
- 总大小和磁盘占比显示正确
- 数据详情列表显示每类数据大小
- 刷新按钮可以重新扫描
- 清理按钮可以清理对应数据

View File

@@ -0,0 +1,13 @@
# Checklist
- [ ] `AppSettingsSnapshot.BackToWindowsButtonDisplayMode` exists and defaults to `IconAndText`.
- [ ] `AppSettingsSnapshot` contains icon source, Fluent icon name, and text icon settings with safe defaults.
- [ ] General > Basic Settings includes one folded back-to-platform button settings expander.
- [ ] The expander includes the display-mode dropdown.
- [ ] The expander includes nested icon source, Fluent icon popup picker, and text icon input controls.
- [ ] The Dock button left icon slot renders either a Fluent icon or custom text.
- [ ] `IconAndText`, `IconOnly`, and `TextOnly` modes update the Dock button live.
- [ ] Icon source, Fluent icon name, and text icon updates refresh the Dock button live.
- [ ] The selected mode is preserved when MainWindow saves app settings.
- [ ] Localization keys exist for zh-CN, en-US, ja-JP, and ko-KR.
- [ ] `dotnet build LanMountainDesktop.slnx -c Debug` succeeds.

View File

@@ -0,0 +1,29 @@
# Dock Back To Windows Button Display
## Summary
The Dock "Back to platform" action should expose a configurable left icon slot while keeping the localized platform text fixed.
## Requirements
- The default display mode is `IconAndText` so existing users keep a familiar Dock layout after upgrade.
- The localized platform text remains controlled by the app and is not user-editable.
- General > Basic Settings exposes one Fluent Avalonia `FASettingsExpander` for the back-to-platform button, with icon-related controls folded into nested `FASettingsExpanderItem` rows.
- The main row exposes a dropdown with `IconAndText`, `IconOnly`, and `TextOnly` options.
- A nested icon source row selects Fluent icon or text icon.
- Fluent icon mode uses a popup picker-style flyout with search and a grid of the full FluentIcons `Icon` enum.
- Text icon mode lets the user enter short text for the left icon slot.
- Changing the dropdown persists to `AppSettingsSnapshot.BackToWindowsButtonDisplayMode` and updates the Dock button without restarting.
- Changing the icon source, Fluent icon, or text icon persists to app settings and updates the Dock button without restarting.
- `IconOnly` keeps the existing tooltip text so the button remains understandable.
- `PinnedTaskbarActions` continues to control whether the action is visible; it does not replace the display mode setting.
## Acceptance Scenarios
- With default settings, the Dock button shows a small circle icon and the localized platform text.
- Selecting icon only hides the platform text and keeps the configured left icon visible.
- Selecting text only hides the left icon slot and keeps the localized platform text visible.
- Choosing a Fluent icon changes the left icon slot.
- Entering a short text icon changes the left icon slot.
- Restarting the app restores the selected display mode.
- Clicking the button still runs the existing minimize/back-to-platform behavior.

View File

@@ -0,0 +1,14 @@
- [x] ComponentCategoryIconResolver 基于 IconKey 正确解析分类图标
- [x] IconKey 为 "Clock" 时解析为 Icon.Clock
- [x] IconKey 为 "WeatherSunny" 时解析为 Icon.WeatherSunny
- [x] IconKey 为 "News" 时解析为 Icon.News
- [x] IconKey 为 "Edit" 时解析为 Icon.Edit
- [x] IconKey 为无效值时回退到 Icon.Apps
- [x] 分类 ID 为 "all" 时返回 Icon.Apps
- [x] ComponentLibraryCategoryViewModel.Icon 类型为 FluentIcons.Common.Icon
- [x] FusedDesktopComponentLibraryControl.axaml.cs 不再包含硬编码 ResolveCategoryIcon 方法
- [x] ComponentLibraryWindow.axaml.cs 不再包含硬编码 ResolveCategoryIcon 方法
- [x] MainWindow.ComponentSystem.cs 不再包含硬编码 ResolveComponentLibraryCategoryIcon 方法
- [x] 三处组件库入口对同一分类显示相同图标
- [x] dotnet build 无编译错误
- [x] dotnet test 全部通过

View File

@@ -0,0 +1,73 @@
# 融合桌面组件库分类图标统一规格
## Why
融合桌面组件库窗口FusedDesktopComponentLibraryControl的分类图标使用了手动硬编码的 `ResolveCategoryIcon` 方法映射分类 ID 到 `Symbol` 枚举与阑山桌面主窗口MainWindow中的映射存在不一致例如 `Info` 分类在主窗口映射到 `Symbol.Apps`,在融合桌面映射到 `Symbol.Info`)。同时,`DesktopComponentDefinition.IconKey` 字段已经存储了正确的 FluentIcon 枚举名称字符串,但未被利用。需要统一三处图标映射逻辑,确保所有组件库入口的分类图标一致且正确。
## What Changes
- **统一分类图标映射**:将三处分散的 `ResolveCategoryIcon`/`ResolveComponentLibraryCategoryIcon` 方法合并为共享的统一映射
- **使用 `IconKey` 驱动图标**:分类图标应基于该分类下组件的 `IconKey` 字段推导,而非硬编码的分类 ID 映射
- **使用 `FluentIcons.Common.Icon` 枚举**`fi:FluentIcon` 控件使用 `Icon` 枚举(非 `Symbol` 枚举),分类图标应使用 `Icon` 枚举以与 `fi:FluentIcon` 兼容
- **修改 ViewModel**`ComponentLibraryCategoryViewModel.Icon` 属性类型从 `Symbol` 改为 `Icon`
## Impact
- 受影响文件:
- `LanMountainDesktop/ViewModels/ComponentLibraryWindowViewModel.cs`Icon 属性类型从 Symbol 改为 Icon
- `LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml`(绑定路径不变,但 Icon 类型变化)
- `LanMountainDesktop/Views/FusedDesktopComponentLibraryControl.axaml.cs`(移除硬编码映射,使用统一方法)
- `LanMountainDesktop/Views/ComponentLibraryWindow.axaml.cs`(移除硬编码映射,使用统一方法)
- `LanMountainDesktop/Views/MainWindow.ComponentSystem.cs`(移除硬编码映射,使用统一方法)
- 新增共享映射工具类(或在现有服务中添加)
## ADDED Requirements
### Requirement: 统一分类图标映射
系统 SHALL 提供一个共享的分类图标映射方法,所有组件库入口(阑山桌面主窗口、融合桌面组件库、独立组件库窗口)均使用此方法。
#### Scenario: 图标映射来源
- **GIVEN** 一个组件分类 ID
- **WHEN** 需要获取该分类的图标
- **THEN** 系统应基于该分类下组件的 `IconKey` 字段推导分类图标
- **AND** 推导规则为:取该分类下第一个组件的 `IconKey`,解析为 `FluentIcons.Common.Icon` 枚举值
- **AND** 若 `IconKey` 无法解析为有效的 `Icon` 枚举值,则回退到 `Icon.Apps`
#### Scenario: 特殊分类处理
- **GIVEN** 分类 ID 为 "all"
- **WHEN** 需要获取该分类的图标
- **THEN** 系统应返回 `Icon.Apps`
#### Scenario: 三处映射一致性
- **GIVEN** 任意一个组件分类
- **WHEN** 在阑山桌面主窗口、融合桌面组件库、独立组件库窗口中显示该分类
- **THEN** 三处应显示完全相同的图标
### Requirement: ViewModel 使用 Icon 枚举
`ComponentLibraryCategoryViewModel.Icon` 属性 SHALL 使用 `FluentIcons.Common.Icon` 枚举类型(而非 `FluentIcons.Common.Symbol`),以与 `fi:FluentIcon` 控件的 `Icon` 属性兼容。
#### Scenario: XAML 绑定兼容
- **GIVEN** `ComponentLibraryCategoryViewModel.Icon` 属性类型为 `Icon`
- **WHEN** 在 XAML 中通过 `{Binding Icon}` 绑定到 `fi:FluentIcon` 控件
- **THEN** 图标应正确渲染,无需额外转换
## MODIFIED Requirements
### Requirement: 分类图标解析
原实现使用硬编码的 `if/switch` 语句将分类 ID 映射到 `Symbol` 枚举,新实现改为:
- 使用 `DesktopComponentDefinition.IconKey` 字段作为图标来源
- 通过 `Enum.TryParse<Icon>(iconKey, ignoreCase: true, out var icon)` 解析
- 解析失败时回退到 `Icon.Apps`
- 移除所有三处硬编码映射方法
### Requirement: ComponentLibraryCategoryViewModel.Icon 类型
原类型为 `Symbol`,修改为 `Icon`,与 `fi:FluentIcon` 控件的 `Icon` 依赖属性类型一致。
## REMOVED Requirements
无移除的需求。

View File

@@ -0,0 +1,38 @@
# Tasks
- [x] Task 1: 创建共享分类图标映射工具
- [x] SubTask 1.1: 在 `LanMountainDesktop.ComponentSystem` 命名空间下创建 `ComponentCategoryIconResolver` 静态类
- [x] SubTask 1.2: 实现 `ResolveCategoryIcon(string categoryId, IEnumerable<DesktopComponentDefinition> categoryComponents)` 方法,基于 IconKey 解析为 `FluentIcons.Common.Icon`
- [x] SubTask 1.3: 添加单元测试验证图标解析逻辑TDD先写失败测试再实现
- [x] Task 2: 修改 ViewModel 的 Icon 属性类型
- [x] SubTask 2.1: 将 `ComponentLibraryCategoryViewModel.Icon` 属性类型从 `Symbol` 改为 `Icon`
- [x] SubTask 2.2: 更新构造函数参数类型
- [x] Task 3: 更新 FusedDesktopComponentLibraryControl.axaml.cs
- [x] SubTask 3.1: 移除 `ResolveCategoryIcon` 硬编码方法
- [x] SubTask 3.2: 在 `LoadCategories` 中使用 `ComponentCategoryIconResolver.ResolveCategoryIcon`
- [x] SubTask 3.3: 更新 "all" 分类图标从 `Symbol.Apps` 改为 `Icon.Apps`
- [x] Task 4: 更新 ComponentLibraryWindow.axaml.cs
- [x] SubTask 4.1: 移除 `ResolveCategoryIcon` 硬编码方法
- [x] SubTask 4.2: 使用 `ComponentCategoryIconResolver.ResolveCategoryIcon`
- [x] Task 5: 更新 MainWindow.ComponentSystem.cs
- [x] SubTask 5.1: 移除 `ResolveComponentLibraryCategoryIcon` 硬编码方法
- [x] SubTask 5.2: 使用 `ComponentCategoryIconResolver.ResolveCategoryIcon`
- [x] SubTask 5.3: 更新 `ComponentLibraryCategory` 记录的 `Icon` 字段类型从 `Symbol` 改为 `Icon`
- [x] SubTask 5.4: 更新 `GetComponentLibraryCategories` 方法中的图标解析调用
- [x] Task 6: 更新 XAML 绑定
- [x] SubTask 6.1: 验证 `FusedDesktopComponentLibraryControl.axaml``fi:FluentIcon Icon="{Binding Icon}"` 绑定在新类型下正常工作
- [x] Task 7: 构建验证
- [x] SubTask 7.1: 运行 `dotnet build` 确保无编译错误
- [x] SubTask 7.2: 运行 `dotnet test` 确保所有测试通过
# Task Dependencies
- Task 2 依赖于 Task 1共享映射工具
- Task 3、4、5 依赖于 Task 1 和 Task 2
- Task 6 依赖于 Task 2类型变更后验证绑定
- Task 7 依赖于所有前置任务

View File

@@ -0,0 +1,10 @@
# Checklist
- [x] `LanMountainDesktop.Shared.IPC` builds in Debug.
- [x] `LanMountainDesktop.Launcher` builds in Debug.
- [x] `LanMountainDesktop` builds in Debug.
- [x] `LanMountainDesktop.AirAppHost` builds in Debug.
- [x] `LanMountainDesktop.Tests` builds in Debug.
- [x] Air APP launcher and lifecycle unit tests pass.
- [x] Direct-host fallback starts Launcher in `air-app-broker` mode instead of debug/normal launch mode.
- [ ] Manual process-lifetime verification with the running desktop.

View File

@@ -0,0 +1,22 @@
# Launcher Managed Air APP Lifecycle
## Goal
Make Launcher the authoritative lifecycle manager for built-in Air APP processes. The desktop host requests Air APP operations through IPC, while Launcher creates, activates, tracks, and cleans up Air APP host processes.
## Behavior
- Launcher exposes `IAirAppLifecycleService` on the dedicated `LanMountainDesktop.Launcher.AirApp.v1` pipe.
- Desktop host calls Launcher IPC for `world-clock` and `whiteboard`; it does not directly start `LanMountainDesktop.AirAppHost`.
- If the dedicated pipe is unavailable, the desktop host starts Launcher with the hidden `air-app-broker --requester-pid <pid>` command and retries the Air APP request.
- `air-app-broker` starts only the Air APP lifecycle IPC broker. It bypasses OOBE, Splash, debug preview windows, and normal desktop launch orchestration.
- Launcher keeps one Air APP process per `{appId}:{sourceComponentId}:{sourcePlacementId}` key.
- AirAppHost receives Launcher pipe and instance key at startup, registers after the window opens, and unregisters on close.
- Launcher remains alive while the main desktop process or any Air APP process is alive.
- Broker mode remains alive while the requester desktop process or any Air APP process is alive; after both are gone, it exits.
## Out of Scope
- Third-party plugin-declared Air APP metadata.
- Cross-machine IPC.
- Persisting the Air APP instance table across OS reboot.

View File

@@ -0,0 +1,11 @@
# Tasks
- [x] Add shared Air APP lifecycle IPC contracts.
- [x] Add Launcher Air APP lifecycle service and dedicated IPC host.
- [x] Make Launcher remain alive while desktop or Air APP processes exist.
- [x] Route desktop Air APP launch requests through Launcher IPC.
- [x] Add hidden `air-app-broker` Launcher command for direct-host development fallback.
- [x] Make desktop fallback start `air-app-broker --requester-pid <pid>` instead of normal `launch`.
- [x] Add broker lifetime and command recognition tests.
- [x] Add AirAppHost registration and unregister best-effort calls.
- [x] Add lifecycle service and request-building tests.

View File

@@ -6,3 +6,4 @@
- [ ] `apply-update` and `plugin-install` do not auto-enter OOBE.
- [ ] Default plugin install does not request UAC.
- [ ] Logs include OOBE status, suppression reason, and launch source.
- [ ] Startup presentation step inside `OobeWindow` (after data location) writes host `settings.json` and syncs Windows Run when autostart is chosen (Launcher executable).

View File

@@ -65,3 +65,19 @@
- 托盘失败时应用仍保持可恢复。
- Launcher 与应用设置页显示相同版本。
- 100% / 150% / 200% / 250% 缩放下Launcher OOBE、主窗口入场、通知位置与动画正常。
### 5. Launcher IPC and error surface follow-up
- The legacy `LanMountainDesktop_Launcher` named-pipe startup progress channel is retired. Public IPC notifications and host exit codes are the only startup state sources.
- Normal Launcher launches must probe public IPC for an existing Host before starting a new Host process. Host no longer owns multi-instance policy, activation prompts, or the old single-instance pipe.
- `SecondaryActivationSucceeded` is a success terminal state. `SecondaryActivationFailed` and `RestartLockNotAcquired` may surface as failures only after public IPC recovery has failed.
- Launcher startup errors must use FluentAvalonia resources, Fluent icons, an InfoBar recovery hint, and copyable diagnostics instead of the old hard-coded dark panel.
### 6. Multi-instance behavior setting
- App settings include `MultiInstanceLaunchBehavior` with default `NotifyAndOpenDesktop`.
- General settings exposes the behavior under Basic Settings with four choices: restart app, open desktop silently, prompt only, and notify plus open desktop.
- Launcher reads the Host `settings.json` before a normal launch and applies the selected behavior when public IPC reports an existing Host.
- `PromptOnly` shows a Fluent Launcher prompt and does not open the desktop automatically.
- `NotifyAndOpenDesktop` activates the existing Host and shows the already-running notice from Launcher.
- `RestartApp` requests restart through public IPC and must not create a second Host if the restart request fails.

View File

@@ -12,3 +12,10 @@
- [x] 修复主窗口入场、通知定位和 Launcher OOBE 的高分屏动画/定位问题。
- [x] 补充规格与版本同步说明文档。
- [ ] 追加针对托盘恢复和启动判定的自动化回归测试。
- [x] Remove the legacy `LanMountainDesktop_Launcher` startup progress pipe; launcher progress now uses public IPC plus host exit-code classification only.
- [x] Move normal multi-open probing into Launcher before host launch and remove Host-side single-instance prompt/listener code.
- [x] Refresh the Launcher error window with Fluent resources, InfoBar, Fluent icons, command bar actions, and copyable diagnostic details.
- [x] Add app-level `MultiInstanceLaunchBehavior` setting and expose it in General > Basic Settings.
- [x] Make Launcher apply restart/open silently/prompt only/notify and open behavior before starting a new Host.
- [x] Add a Fluent Launcher multi-instance prompt; Host public IPC stays limited to activation/status/restart/exit actions.

View File

@@ -4,14 +4,14 @@
- Tray menu `Exit App` must commit an irreversible host shutdown request.
- Once shutdown is committed, tray menu actions must not reopen the desktop, settings window, or component library.
- Shutdown cleanup must release Public IPC, plugin runtime, tray icon, fused desktop edit UI, telemetry resources, and the single-instance lock before the forced-exit deadline.
- Shutdown cleanup must release Public IPC, plugin runtime, tray icon, fused desktop edit UI, and telemetry resources before the forced-exit deadline.
- Forced process termination must be scheduled when the shutdown request is accepted, not only after Avalonia lifetime exit.
- Restart must preserve `RestartRequested` intent and must not route through an exit path that overwrites it.
- Fused desktop component library menu activation must reuse the existing library window and must exit edit mode if opening fails.
## Acceptance
- Selecting `Exit App` from the tray leaves no background host process and allows a later Launcher start to acquire the single-instance lock.
- Selecting `Exit App` from the tray leaves no background host process and allows a later Launcher start to perform multi-instance detection through public IPC.
- Selecting `Restart App` starts the Launcher or upgrade helper once, then shuts down the old host as a restart.
- Repeated tray clicks during shutdown are ignored and logged.
- Repeated component-library clicks focus the existing window instead of opening duplicates.

View File

@@ -0,0 +1,42 @@
# Main Window Desktop Layer Design
## Window Roles
Lan Mountain Desktop now has three separate window-layer roles:
- `MainDesktopWindow`: the normal desktop host window. With `EnableMainWindowDesktopLayer`, this window is moved to the desktop layer so it does not cover ordinary apps.
- `FusedDesktopSurface`: fused desktop component windows such as `DesktopWidgetWindow` and `TransparentOverlayWindow`. These continue to use `IWindowBottomMostService` and their existing click-through region service.
- `AirApp`: independent Air APP windows. These are ordinary app windows and do not use desktop-layer services or global `Topmost` promotion.
## Service Boundary
`IMainWindowDesktopLayerService` is dedicated to the main window only. It does not reuse fused desktop passthrough services because the main window must stay interactive.
Windows behavior:
- Save original parent, style, and extended style before enabling.
- Try to attach the main window to the desktop icon host.
- If that host is not found, use `HWND_BOTTOM`.
- On disable, restore the saved parent and styles as best effort.
Non-Windows behavior:
- Keep a null implementation.
- Log that the platform is unsupported.
## Settings Flow
The developer settings page owns confirmation UX for conflicts:
- Fused desktop toggle and main-window desktop-layer toggle are one-way bound.
- Toggle click handlers ask for confirmation before saving conflicting states.
- The view model writes both keys together so runtime listeners receive a coherent change set.
## Runtime Flow
Main-window restore paths call `ActivateOrRefreshMainWindowLayer`.
- If `EnableMainWindowDesktopLayer` is enabled, the app refreshes the desktop-layer attachment and hides the taskbar entry.
- If disabled, the app restores ordinary activation behavior, including the existing temporary foreground promotion.
Settings changes call both fused desktop and main-window desktop-layer runtime application paths so switching modes is immediate.

View File

@@ -0,0 +1,20 @@
# Main Window Desktop Layer
## Requirements
- Add a developer option named `EnableMainWindowDesktopLayer`.
- When enabled, the main Lan Mountain desktop window behaves like a desktop-surface window: ordinary application windows can stay above it.
- The feature is implemented as desktop-layer or bottom placement, not as `Topmost`.
- The option is mutually exclusive with `EnableFusedDesktop`.
- Enabling main-window desktop layer while fused desktop is enabled must ask for confirmation, then disable fused desktop on confirm or roll back on cancel.
- Enabling fused desktop while main-window desktop layer is enabled must ask for confirmation, then disable main-window desktop layer on confirm or roll back on cancel.
- Air APP windows remain ordinary application windows and must not be attached to the desktop layer.
- On Windows, the main window should attach to the desktop icon host when available and fall back to `HWND_BOTTOM` when unavailable.
- On non-Windows platforms, the setting may exist but the layer service is a no-op and must not throw.
## Acceptance
- Opening another app above Lan Mountain Desktop keeps that app visible when main-window desktop layer is enabled.
- Restoring the main window from tray keeps the desktop-layer behavior and does not perform a temporary `Topmost` promotion.
- Turning the option off restores normal main-window behavior as far as possible.
- Fused desktop component windows keep their existing bottom-most behavior and remain isolated from the main-window service.

View File

@@ -0,0 +1,10 @@
# Main Window Desktop Layer Tasks
- [x] Add `EnableMainWindowDesktopLayer` to app settings with a disabled default.
- [x] Add developer settings UI and localization strings.
- [x] Add confirmation flow for mutual exclusion with fused desktop.
- [x] Add a dedicated main-window desktop-layer service.
- [x] Wire main-window creation, restore, tray fallback, settings changes, and shutdown cleanup to the service.
- [x] Keep Air APP windows outside this layer service.
- [x] Add static regression tests for settings, restore paths, and service boundaries.
- [ ] Perform manual Windows z-order validation with real apps.

View File

@@ -0,0 +1,12 @@
# Material Color Service Acceptance Checklist
- [x] `dotnet build LanMountainDesktop.slnx -c Debug` succeeds.
- [x] `dotnet test LanMountainDesktop.slnx -c Debug` succeeds.
- [x] Material & Color page exposes color source, wallpaper source, system material, native event preference, polling interval, manual refresh, semantic color preview, and surface preview.
- [x] Appearance page no longer owns duplicate visible color/material controls.
- [x] Appearance page view model preserves Material & Color settings instead of rewriting them.
- [x] Component corner-radius settings preserve Material & Color fields instead of resetting them through old positional constructors.
- [x] Component editor receives colors from `MaterialColorSnapshot`.
- [x] Plugin SDK snapshot includes read-only color/material fields without breaking the existing constructor shape.
- [x] Wallpaper source selection supports auto, app, and system modes.
- [x] Native wallpaper event monitoring can be disabled and polling remains available.

View File

@@ -0,0 +1,62 @@
# Material Color Service
## Goal
Unify Monet seed extraction, wallpaper color extraction, semantic color roles, host material surfaces, and plugin appearance snapshots behind one host-owned material/color source of truth.
## Scope
- Host service: `IMaterialColorService`
- Compatibility facade: `IAppearanceThemeService`
- Settings page: `MaterialColorSettingsPage`
- Persisted settings:
- `ThemeColorMode`
- `ThemeColor`
- `SelectedWallpaperSeed`
- `SystemMaterialMode`
- `ThemeWallpaperColorSource`
- `UseNativeWallpaperChangeEvents`
- `SystemWallpaperRefreshIntervalSeconds`
- Plugin read-only appearance snapshot fields:
- accent color
- seed color
- color source
- system material mode
- semantic color roles
- material surfaces
- wallpaper seed candidates
## Behavior
`IMaterialColorService` owns the live `MaterialColorSnapshot`. Consumers should derive colors and material values from this snapshot instead of recalculating from raw theme settings, wallpaper settings, or `MonetPalette`.
Supported color sources:
- `default_neutral`: stable neutral surfaces with the default accent.
- `seed_monet`: user-selected seed color processed through Monet.
- `wallpaper_monet`: wallpaper colors processed through Monet.
Wallpaper color source selection:
- `auto`: app wallpaper or app solid color first, then system wallpaper, then fallback.
- `app`: app wallpaper or app solid color only, then fallback.
- `system`: system wallpaper only, then fallback.
System wallpaper monitoring:
- Native Windows user preference events are preferred when enabled and available.
- Polling remains active as the fallback path.
- Manual refresh clears cached wallpaper candidates and rebuilds the snapshot.
## Refactor Rules
- New consumers must depend on `IMaterialColorService`, not on parallel combinations of theme settings, wallpaper settings, and `MonetColorService`.
- `MonetColorService` remains the extraction/palette utility, not the application-wide coordinator.
- Component/editor/plugin appearance code must consume `MaterialColorSnapshot` or a mapper produced from it.
- Existing `IAppearanceThemeService` remains available for compatibility, but it must not become a second source of truth.
## Out Of Scope
- Plugin write access to global host appearance settings.
- Market metadata or sample plugin changes.
- Replacing the wallpaper picker page. It remains the asset/source management page.

View File

@@ -0,0 +1,13 @@
# Material Color Service Tasks
- [x] Add unified material/color snapshot models and `IMaterialColorService`.
- [x] Persist wallpaper color source and native wallpaper event preference.
- [x] Add the Material & Color settings page.
- [x] Keep Appearance focused on theme mode, window chrome, and corner radius.
- [x] Route plugin appearance snapshots through the material/color snapshot.
- [x] Route component editor theming through the material/color snapshot.
- [x] Remove legacy color/material preview and save logic from the Appearance page view model.
- [x] Replace legacy positional `ThemeAppearanceSettingsState` writes with preserving `with` updates where found.
- [x] Keep native wallpaper events optional with polling/manual refresh fallback.
- [x] Add regression tests for normalization, plugin mapping, and component editor palette mapping.
- [ ] Continue retiring legacy direct consumers of raw theme/wallpaper/Monet tuples when they are touched.

View File

@@ -1,13 +1,16 @@
# Checklist
- [ ] `release.yml` includes PDCC publish flow and does not invoke Velopack.
- [ ] `release.yml` uploads app payload artifacts for PDCC.
- [ ] S3 output path is rooted at `lanmountain/update/` (no system version prefix).
- [ ] S3 has `repo/`, `meta/`, and `installers/` outputs after a release run.
- [ ] Host update source default is `stcn` and old `pdc` values are auto-normalized.
- [ ] Host can persist PDC payload into launcher incoming directory.
- [ ] Launcher can apply PDC FileMap payload with signature/hash verification.
- [ ] Legacy signed `files.json + update.zip` path still works as compatibility fallback.
- [x] `release.yml` does not invoke Velopack.
- [x] `plonds-build.yml` uploads app payload artifacts and generates PloNDS delta/static outputs.
- [x] S3 output path is rooted at `lanmountain/update/` (no system version prefix).
- [x] CI workflow expects `repo/`, `meta/`, `manifests/`, and `installers/` outputs after a release run.
- [x] Host update source keeps compatibility (`pdc`/`stcn` normalize to active PloNDS source).
- [x] Host can persist PloNDS payload into launcher incoming directory.
- [x] Launcher can apply PloNDS FileMap payload with signature/hash verification.
- [x] Legacy signed `files.json + update.zip` path still works as compatibility fallback.
- [x] Launcher keeps rollback-capable deployments after successful update.
- [x] Manual rollback returns a structured failure when the snapshot source directory is missing.
- [ ] CI run attached proving all release matrix jobs pass.
- [ ] N-1 -> N incremental update verified on Windows x64/x86 and Linux x64.
- [ ] Rollback verification report attached.
- [x] N-1 -> N incremental update verified locally on Windows x64.
- [ ] N-1 -> N incremental update verified on Windows x86 and Linux x64.
- [x] Rollback regression tests attached in `LanMountainDesktop.Tests`.

View File

@@ -12,29 +12,33 @@ Replace VeloPack-based incremental packaging with a unified PDC FileMap + object
## Stage 2 (Current Implementation Target)
- Move release publishing to PDCC + `phainon.yml` (ClassIsland-style).
- Promote PDC-distributed FileMap/object-repo as the primary incremental path.
- Use GitHub Actions PloNDS static publishing as the active incremental path.
- Keep `phainon.yml` for future PDCC parity, but do not rely on PDCC for the current release flow.
- Promote PloNDS-distributed FileMap/object-repo as the primary incremental path.
- Keep GitHub Release installers and metadata as parallel distribution.
- Keep Launcher state machine ownership (`.current/.partial/.destroy` + snapshots).
- Update source defaults to `stcn` (S3/PDC), with GitHub fallback.
- Check updates in order: NS3/PloNDS static source, GitHub Release PloNDS assets, then GitHub full installer.
- S3 object root is fixed to `lanmountain/update/` with no update-system version prefix.
- Public object URLs come from `S3_PUBLIC_BASE_URL`; do not infer them from `S3_ENDPOINT` and `S3_BUCKET`.
Expected S3 layout:
- `lanmountain/update/repo/<hash-prefix>/<hash-object>`
- `lanmountain/update/meta/channels/<channel>/<subchannel>/latest.json`
- `lanmountain/update/meta/distributions/<distributionId>/*.json`
- `lanmountain/update/installers/<platform>/<arch>/*`
- `lanmountain/update/repo/sha256/<hash-prefix>/<hash-object>`
- `lanmountain/update/meta/channels/<channel>/<platform>/latest.json`
- `lanmountain/update/meta/distributions/<distributionId>.json`
- `lanmountain/update/manifests/<distributionId>/plonds-filemap.json`
- `lanmountain/update/manifests/<distributionId>/plonds-filemap.json.sig`
- `lanmountain/update/installers/<platform>/<version>/*`
## Acceptance
- `release.yml` includes PDCC publish steps and no Velopack steps.
- `release.yml` contains no Velopack steps; PloNDS static publishing is handled by `plonds-build.yml` and `ddss-publish.yml`.
- Release jobs keep building installers for Windows x64/x86, Linux x64, and macOS.
- PDC metadata + FileMap + object repo are published under `lanmountain/update/`.
- Host can consume PDC payload (`stcn` source) and fallback to GitHub when unavailable.
- PloNDS metadata + FileMap + object repo are published under `lanmountain/update/`.
- Host can consume the NS3/PloNDS static payload and fallback to GitHub when unavailable.
- Launcher can apply both:
- legacy signed `files.json + update.zip`
- PDC FileMap object-repo payload.
- Rollback semantics remain unchanged.
- PloNDS FileMap object-repo payload.
- Rollback semantics keep both automatic failure rollback and manual rollback after a successful update.
## Deprecated Notes

View File

@@ -3,13 +3,19 @@
- [x] Remove VeloPack packaging from release workflow.
- [x] Keep signed FileMap path as interim compatibility fallback.
- [x] Remove launcher/runtime Velopack branching.
- [ ] Add `phainon.yml` for PDCC publish configuration.
- [ ] Add PDCC installation + publish steps in `release.yml`.
- [ ] Upload app payload artifacts for PDCC consumption in release build jobs.
- [ ] Publish PDC metadata + object repo to S3 path root `lanmountain/update/`.
- [ ] Mirror installers to `lanmountain/update/installers/<platform>/<arch>/`.
- [ ] Replace update source canonical value with `stcn` (keep legacy `pdc` compatibility).
- [ ] Add PDC payload model into host update check result.
- [ ] Add host download path for PDC payload (`pdc-filemap.json` + signature + metadata).
- [ ] Add launcher PDC FileMap apply path with rollback-compatible semantics.
- [ ] Keep old `files.json + update.zip` path behind compatibility fallback.
- [x] Add `phainon.yml` for PDCC publish configuration.
- [ ] Add PDCC installation + publish steps in `release.yml` (deferred; active path is GitHub Actions PloNDS static publish).
- [x] Upload app payload artifacts for PloNDS delta generation in release build jobs.
- [x] Publish PloNDS metadata + sha256 object repo to S3 path root `lanmountain/update/`.
- [x] Mirror installers to `lanmountain/update/installers/<platform>/<version>/`.
- [x] Keep update source compatibility (`pdc`/`stcn` normalize to active PloNDS source).
- [x] Add PloNDS static payload model into host update check result.
- [x] Add host download path for PloNDS payload (`plonds-filemap.json` + signature + object repo).
- [x] Add launcher PloNDS FileMap apply path with rollback-compatible semantics.
- [x] Keep old `files.json + update.zip` path behind compatibility fallback.
- [x] Keep rollback deployment directories after successful apply and prune by bounded retention.
- [x] Return structured failure when manual rollback snapshot source is missing.
- [x] Verify static S3 layout, filemap/signature, distribution, latest pointer, and at least one object in CI workflows.
- [x] Add regression tests for PloNDS success rollback, hash-failure auto rollback, missing rollback source, static NS3 manifest, and manifest field mapping.
- [ ] Attach live CI run proving the full release matrix passes.
- [ ] Verify N-1 -> N incremental update on Windows x86 and Linux x64 in release artifacts.

View File

@@ -0,0 +1,25 @@
# Settings Window Fluent Shell Redesign
## Goal
Rebuild the settings window as an independent Fluent shell with a custom titlebar, titlebar hamburger menu, persistent side navigation, search, and Avalonia-standard system material support.
## Requirements
- Keep the existing independent settings-window lifecycle: open-or-focus, no owner anchor, own taskbar entry.
- Use a 48 DIP titlebar with Back, pane toggle, icon/title, search, restart action, more menu, and caption-button spacer.
- Keep `FANavigationView` as the primary navigation surface with `OpenPaneLength` around 283 DIP.
- Move the compact/minimal pane toggle from the navigation footer into the titlebar.
- Add search over built-in settings pages and settings expanders; selecting a result navigates, expands, focuses, and highlights.
- Add `auto` system material mode and make it the default.
- Implement material with Avalonia `TransparencyLevelHint` only.
- Preserve settings page layout as direct `ScrollViewer -> StackPanel -> FASettingsExpander` content.
- Follow `docs/VISUAL_SPEC.md`, `docs/CORNER_RADIUS_SPEC.md`, and `docs/ai/SETTINGS_WINDOW_DESIGN.md`.
## Acceptance
- `dotnet build LanMountainDesktop.slnx -c Debug` succeeds.
- `dotnet test LanMountainDesktop.slnx -c Debug` succeeds or any unrelated failures are documented.
- The settings window can navigate by sidebar, titlebar Back, titlebar pane toggle, and search.
- Appearance settings expose Auto, None, Mica, and/or Acrylic according to system support.
- Existing dirty user changes are not reverted.

View File

@@ -0,0 +1,13 @@
# Tasks
- [x] Analyze current `SettingsWindow`, appearance theme service, and existing settings page layout.
- [x] Compare ClassIsland `SettingsWindowNew` and SecRandom v3 Avalonia `SettingsView`.
- [x] Replace footer fallback pane toggle with titlebar pane toggle.
- [x] Add titlebar Back, search, restart, and more-options controls.
- [x] Add settings navigation history.
- [x] Add settings search service and result highlight.
- [x] Add `auto` system material mode and Avalonia `TransparencyLevelHint` priority.
- [x] Update appearance settings options and localization.
- [x] Add focused tests for material normalization and search filtering.
- [x] Add design/spec documentation.
- [ ] Run full app manually on Windows 11 and Windows 10 to verify actual Mica/Acrylic backdrops.

View File

@@ -0,0 +1,25 @@
# Update Settings Fluent Controls
## Goal
Make the Settings > Update page the single user-facing control surface for the host update flow.
## Requirements
- The page uses Fluent Avalonia settings controls for update status, release facts, update behavior, and transfer controls.
- Users can choose update channel, download source, update mode, and download thread count.
- Update mode options are:
- Manual: do not automatically download or install.
- Silent Download: check and download in the background, then wait for user installation confirmation.
- Silent Install: check and download in the background, then apply when the app exits.
- Users can opt into forced reinstall. When enabled, the update check targets the current version manifest where available and the UI labels the next payload as reinstall.
- The page displays whether the current payload is an incremental update or reinstall/full installer.
- The page exposes pause, resume, and cancel actions for resumable downloads and install recovery.
- Existing PloNDS/FileMap incremental update and Launcher rollback ownership remain unchanged.
## Acceptance
- `UpdateSettingsPage` shows Fluent Avalonia controls for channel, mode, thread count, forced reinstall, pause/resume, and cancel.
- `UpdateSettingsState` persists forced reinstall alongside other update preferences.
- Automatic startup checks skip manual mode, download in silent download/silent install modes, and leave installation to explicit user action or exit-time apply.
- Build succeeds for `LanMountainDesktop.slnx`.

View File

@@ -0,0 +1,7 @@
# Checklist
- [x] Air APP window code does not call fused desktop bottom-most APIs.
- [x] Air APP window code does not set `Topmost = true`.
- [x] Fused desktop overlay and widget windows still use bottom-most APIs.
- [x] Fused desktop widget reload path refreshes desktop layer after showing.
- [ ] Manual Windows z-order verification with fused desktop and Air APP windows.

View File

@@ -0,0 +1,18 @@
# Window Layer Isolation
## Goal
Keep fused desktop component windows and Air APP windows in separate z-order roles.
## Behavior
- Fused desktop windows are desktop-surface windows. They may use `IWindowBottomMostService` and region passthrough, must stay attached to the Windows desktop icon host when supported, and must not cover ordinary apps.
- Air APP windows are ordinary application windows. They must not use the fused desktop bottom-most service, must not attach to the desktop icon host, and must not use global `Topmost` promotion.
- Re-showing or reloading fused desktop widgets refreshes their desktop layer after the window is visible.
- Air APP activation uses normal window activation; repeated-open foreground recovery remains owned by Launcher lifecycle activation.
## Out of Scope
- Changing Air APP lifecycle IPC.
- Changing whiteboard note sharing.
- Implementing third-party Air APP SDK behavior.

View File

@@ -0,0 +1,7 @@
# Tasks
- [x] Remove Air APP `Topmost` promotion from `AirAppWindow`.
- [x] Add explicit desktop-layer refresh for fused desktop widget windows.
- [x] Refresh fused desktop widget windows after show/reload.
- [x] Add window-role diagnostics for desktop-surface and Air APP windows.
- [x] Add static regression tests for window-layer isolation.