mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
* 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.天气选项卡更新
778 lines
26 KiB
Markdown
778 lines
26 KiB
Markdown
# 数据设置页实现计划
|
||
|
||
> **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. 点击「数据」选项卡,确认:
|
||
- 堆叠条形图显示各类数据占比
|
||
- 总大小和磁盘占比显示正确
|
||
- 数据详情列表显示每类数据大小
|
||
- 刷新按钮可以重新扫描
|
||
- 清理按钮可以清理对应数据
|