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.
This commit is contained in:
lincube
2026-05-07 16:34:31 +08:00
parent aa7e15d967
commit 84caca02bf
15 changed files with 1812 additions and 9 deletions

View File

@@ -3,6 +3,7 @@
xmlns:sty="using:FluentAvalonia.Styling"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:mi="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:conv="using:LanMountainDesktop.Converters"
x:Class="LanMountainDesktop.App"
xmlns:local="using:LanMountainDesktop"
RequestedThemeVariant="Default">
@@ -12,6 +13,8 @@
<FontFamily x:Key="AppFontFamily">MiSans VF, avares://LanMountainDesktop/Assets/Fonts#MiSans, avares://LanMountainDesktop/Assets/Fonts#MiSans VF</FontFamily>
<FontFamily x:Key="AppFontFamilyJP">avares://LanMountainDesktop/Assets/Fonts#MiSans JP</FontFamily>
<FontFamily x:Key="AppFontFamilyKR">avares://LanMountainDesktop/Assets/Fonts#MiSans KR</FontFamily>
<conv:HexToColorConverter x:Key="HexToColorConverter" />
<conv:HexToBrushConverter x:Key="HexToBrushConverter" />
</Application.Resources>
<Application.DataTemplates>

View File

@@ -0,0 +1,31 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace LanMountainDesktop.Converters;
public sealed 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 Brushes.Gray;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
namespace LanMountainDesktop.Converters;
public class HexToColorConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is string hex && !string.IsNullOrWhiteSpace(hex))
{
try
{
return Color.Parse(hex);
}
catch
{
// Ignore parse errors
}
}
return Colors.Gray;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}

View File

@@ -352,6 +352,8 @@
"settings.general.slide_transition_desc": "Use a slide-in startup transition on supported Windows builds. This option disables fade transition.",
"settings.general.show_main_window_taskbar_header": "Show main desktop window in taskbar",
"settings.general.show_main_window_taskbar_desc": "Keep the main desktop host window visible in the taskbar. The independent settings window always has its own taskbar entry.",
"settings.data.title": "Data",
"settings.data.description": "Review and manage local app storage and cache.",
"settings.appearance.title": "Appearance",
"settings.appearance.description": "Adjust theme source, system material, and window chrome.",
"settings.appearance.theme_header": "Theme",

View File

@@ -287,6 +287,8 @@
"settings.general.preview_time_label": "時刻",
"settings.general.preview_date_label": "日付",
"settings.general.render_mode_restart_message": "レンダリングモードの変更にはアプリの再起動が必要です。",
"settings.data.title": "データ",
"settings.data.description": "ローカルに保存されたアプリデータとキャッシュを確認・管理します。",
"settings.appearance.title": "外観",
"settings.appearance.description": "テーマソース、システムマテリアル、ウィンドウクロームを調整します。",
"settings.appearance.theme_header": "テーマ",

View File

@@ -335,6 +335,8 @@
"settings.general.preview_time_label": "시간",
"settings.general.preview_date_label": "날짜",
"settings.general.render_mode_restart_message": "렌더링 모드 변경은 앱 재시작이 필요합니다.",
"settings.data.title": "데이터",
"settings.data.description": "로컬에 저장된 앱 데이터와 캐시를 확인하고 관리합니다.",
"settings.appearance.title": "외관",
"settings.appearance.description": "테마 소스, 시스템 소재 및 창 외관을 조정합니다.",
"settings.appearance.theme_header": "테마",

View File

@@ -352,6 +352,8 @@
"settings.general.slide_transition_desc": "在受支持的 Windows 版本上使用滑入启动过渡。启用后会关闭淡入淡出过渡。",
"settings.general.show_main_window_taskbar_header": "在任务栏显示主桌面窗口",
"settings.general.show_main_window_taskbar_desc": "让主桌面宿主窗口保持在任务栏中可见。独立设置窗口始终拥有自己的任务栏入口。",
"settings.data.title": "数据",
"settings.data.description": "查看与管理本机存储中的应用数据与缓存。",
"settings.appearance.title": "外观",
"settings.appearance.description": "调整主题来源、系统材质与窗口外观。",
"settings.appearance.theme_header": "主题",

View File

@@ -0,0 +1,357 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
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 string[] SettingsRootFiles =
[
"settings.json",
"plugin-settings.json",
"launcher-settings.json",
"app.db",
"app.db-shm",
"app.db-wal"
];
private static readonly string[] CategoryRelativeDirectories =
[
"Whiteboards",
"Extensions",
"PluginMarket",
"Wallpapers"
];
private static readonly IReadOnlyList<StorageCategoryInfo> Categories = new List<StorageCategoryInfo>
{
new("logs", "日志文件", "应用运行日志", "", true, "#9E9E9E"),
new("whiteboards", "白板笔记", "桌面白板笔记数据", "Whiteboards", true, "#FF9800"),
new("plugins", "插件数据", "已安装插件文件", "Extensions/Plugins", true, "#2196F3"),
new("market", "插件市场缓存", "插件市场元数据缓存", "PluginMarket", true, "#9C27B0"),
new("wallpapers", "壁纸文件", "下载的壁纸资源", "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 pathRoot = Path.GetPathRoot(dataRoot);
if (string.IsNullOrWhiteSpace(pathRoot))
{
return 0;
}
var driveInfo = new DriveInfo(pathRoot);
return driveInfo.TotalSize;
}, cancellationToken);
}
public async Task<long> GetAvailableDiskSpaceAsync(CancellationToken cancellationToken = default)
{
return await Task.Run(() =>
{
var dataRoot = AppDataPathProvider.GetDataRoot();
var pathRoot = Path.GetPathRoot(dataRoot);
if (string.IsNullOrWhiteSpace(pathRoot))
{
return 0;
}
var driveInfo = new DriveInfo(pathRoot);
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(() => GetDirectorySizeCore(path, cancellationToken), cancellationToken);
}
private static async Task<long> GetSettingsSizeAsync(string dataRoot, CancellationToken cancellationToken)
{
return await Task.Run(() =>
{
long size = 0;
var countedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var file in SettingsRootFiles)
{
cancellationToken.ThrowIfCancellationRequested();
var path = Path.Combine(dataRoot, file);
if (File.Exists(path))
{
try
{
var fullPath = Path.GetFullPath(path);
size += new FileInfo(fullPath).Length;
countedFiles.Add(fullPath);
}
catch
{
// Ignore
}
}
}
// Include root-level auxiliary files (exclude known category directories and logs).
try
{
var excludedDirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var relativeDir in CategoryRelativeDirectories)
{
excludedDirs.Add(Path.GetFullPath(Path.Combine(dataRoot, relativeDir)));
}
var logDir = AppLogger.LogDirectory;
if (!string.IsNullOrWhiteSpace(logDir))
{
excludedDirs.Add(Path.GetFullPath(logDir));
}
foreach (var file in Directory.EnumerateFiles(dataRoot, "*", SearchOption.TopDirectoryOnly))
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var info = new FileInfo(file);
if (!info.Exists)
{
continue;
}
var fullPath = Path.GetFullPath(file);
if (countedFiles.Contains(fullPath))
{
continue;
}
size += info.Length;
countedFiles.Add(fullPath);
}
catch
{
// Ignore file probe failures
}
}
foreach (var directory in Directory.EnumerateDirectories(dataRoot, "*", SearchOption.TopDirectoryOnly))
{
cancellationToken.ThrowIfCancellationRequested();
var fullPath = Path.GetFullPath(directory);
if (excludedDirs.Contains(fullPath))
{
continue;
}
size += GetDirectorySizeCore(fullPath, cancellationToken);
}
}
catch
{
// Ignore directory enumeration errors
}
return size;
}, cancellationToken);
}
private static long GetDirectorySizeCore(string path, CancellationToken cancellationToken)
{
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;
}
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"
};
}
}

View File

@@ -0,0 +1,181 @@
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;
}
}
}
}

View File

@@ -0,0 +1,176 @@
<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="0">
<ui:FASettingsExpander Classes="settings-expander-card"
Header="存储概览"
Description="本地应用数据的总占用及对当前磁盘的估算占比。"
IsExpanded="True">
<ui:FASettingsExpander.Footer>
<Button Padding="12,8"
Command="{Binding ScanCommand}"
IsEnabled="{Binding !IsScanning}">
<StackPanel Orientation="Horizontal"
Spacing="8">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular"
FontSize="14" />
<TextBlock Text="重新扫描" VerticalAlignment="Center" />
</StackPanel>
</Button>
</ui:FASettingsExpander.Footer>
<ui:FASettingsExpanderItem>
<StackPanel Spacing="16">
<Border Height="22"
Background="{DynamicResource ControlFillColorTertiaryBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
ClipToBounds="True"
IsVisible="{Binding HasData}">
<Grid x:Name="StorageBarGrid" />
</Border>
<ProgressBar Height="8"
Minimum="0"
Maximum="100"
IsIndeterminate="{Binding IsScanning}"
Value="{Binding DiskUsagePercentage}" />
<Grid ColumnDefinitions="*,Auto"
ColumnSpacing="16">
<StackPanel Spacing="4">
<TextBlock Text="{Binding TotalSizeText}"
FontSize="22"
FontWeight="SemiBold" />
<TextBlock Text="{Binding DiskUsageText}"
FontSize="13"
Opacity="0.72" />
</StackPanel>
</Grid>
<TextBlock Text="正在扫描…"
FontSize="13"
Opacity="0.72"
IsVisible="{Binding IsScanning}" />
<TextBlock Text="暂无可用数据。完成首次扫描后将显示占比与图例。"
FontSize="13"
Opacity="0.72"
TextWrapping="Wrap"
IsVisible="{Binding !HasData}" />
<StackPanel Spacing="10"
IsVisible="{Binding HasData}">
<TextBlock Classes="settings-item-label"
Text="分类图例" />
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:DataStorageItemViewModel">
<StackPanel Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center"
Margin="0,0,16,8">
<Ellipse Width="10"
Height="10"
VerticalAlignment="Center"
Fill="{Binding ColorHex, Converter={StaticResource HexToBrushConverter}}" />
<TextBlock Text="{Binding Name}"
FontSize="13"
VerticalAlignment="Center"
Opacity="0.85" />
<TextBlock Text="{Binding Percentage, StringFormat={}{0:F1}%}"
FontSize="12"
VerticalAlignment="Center"
Opacity="0.65" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</ui:FASettingsExpanderItem>
</ui:FASettingsExpander>
<ui:FASettingsExpander Classes="settings-expander-card"
Header="数据详情"
Description="按类别查看占用;可在允许清理的类别上释放空间。"
IsExpanded="True">
<ui:FASettingsExpanderItem>
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:DataStorageItemViewModel">
<Grid ColumnDefinitions="Auto,*,Auto,Auto"
ColumnSpacing="14"
Margin="0,0,0,18">
<Ellipse Grid.Column="0"
Width="10"
Height="10"
VerticalAlignment="Center"
Fill="{Binding ColorHex, Converter={StaticResource HexToBrushConverter}}" />
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
Spacing="4">
<TextBlock Classes="settings-item-label"
Text="{Binding Name}" />
<TextBlock Classes="settings-item-description"
Text="{Binding Description}" />
</StackPanel>
<TextBlock Grid.Column="2"
Text="{Binding SizeText}"
VerticalAlignment="Center"
FontWeight="SemiBold"
FontSize="13"
Opacity="0.9" />
<Button Grid.Column="3"
Command="{Binding $parent[ItemsControl].((vm:DataSettingsPageViewModel)DataContext).CleanCommand}"
CommandParameter="{Binding Id}"
IsVisible="{Binding IsCleanable}"
IsEnabled="{Binding !IsCleaning}"
VerticalAlignment="Center"
Padding="10,6">
<StackPanel Orientation="Horizontal"
Spacing="6">
<fi:FluentIcon Icon="Delete"
IconVariant="Regular"
FontSize="14" />
<TextBlock Text="清理" VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ui:FASettingsExpanderItem>
</ui:FASettingsExpander>
<Button Classes="settings-accent-button"
Command="{Binding CleanAllCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Margin="0,4,0,0">
<StackPanel Orientation="Horizontal"
Spacing="8">
<fi:FluentIcon Icon="Broom"
IconVariant="Regular"
FontSize="16" />
<TextBlock Text="一键清理所有可清理数据" VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,138 @@
using System;
using System.ComponentModel;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.ViewModels;
namespace LanMountainDesktop.Views.SettingsPages;
[SettingsPageInfo(
"data",
"数据",
SettingsPageCategory.General,
IconKey = "HardDrive",
SortOrder = 5,
TitleLocalizationKey = "settings.data.title",
DescriptionLocalizationKey = "settings.data.description")]
public partial class DataSettingsPage : SettingsPageBase
{
private readonly SolidColorBrush _fallbackBrush = new(Colors.Gray);
public DataSettingsPage()
: this(new DataSettingsPageViewModel())
{
}
public DataSettingsPage(DataSettingsPageViewModel viewModel)
{
ViewModel = viewModel;
DataContext = ViewModel;
InitializeComponent();
AttachStorageBarObservers();
RebuildStorageBar();
}
public DataSettingsPageViewModel ViewModel { get; }
private void AttachStorageBarObservers()
{
ViewModel.PropertyChanged += OnViewModelPropertyChanged;
foreach (var item in ViewModel.Items)
{
item.PropertyChanged += OnStorageItemPropertyChanged;
}
}
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (string.Equals(e.PropertyName, nameof(DataSettingsPageViewModel.Items), StringComparison.Ordinal))
{
RebuildStorageBar();
return;
}
if (string.Equals(e.PropertyName, nameof(DataSettingsPageViewModel.HasData), StringComparison.Ordinal))
{
RebuildStorageBar();
}
}
private void OnStorageItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (string.Equals(e.PropertyName, nameof(DataStorageItemViewModel.Percentage), StringComparison.Ordinal) ||
string.Equals(e.PropertyName, nameof(DataStorageItemViewModel.ColorHex), StringComparison.Ordinal))
{
RebuildStorageBar();
}
}
private void RebuildStorageBar()
{
if (StorageBarGrid is null)
{
return;
}
StorageBarGrid.ColumnDefinitions.Clear();
StorageBarGrid.Children.Clear();
if (!ViewModel.HasData)
{
return;
}
var visibleItems = ViewModel.Items
.Where(item => item.Percentage > 0)
.OrderByDescending(item => item.Percentage)
.ToList();
if (visibleItems.Count == 0)
{
return;
}
var totalPercent = visibleItems.Sum(item => item.Percentage);
if (totalPercent <= 0)
{
return;
}
var idx = 0;
foreach (var item in visibleItems)
{
var normalized = Math.Max(0.1, item.Percentage / totalPercent * 100d);
StorageBarGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(normalized, GridUnitType.Star)));
var segment = new Border
{
Background = ParseBrush(item.ColorHex),
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
Grid.SetColumn(segment, idx++);
StorageBarGrid.Children.Add(segment);
}
}
private IBrush ParseBrush(string? hex)
{
if (string.IsNullOrWhiteSpace(hex))
{
return _fallbackBrush;
}
try
{
return new SolidColorBrush(Color.Parse(hex));
}
catch
{
return _fallbackBrush;
}
}
}

View File

@@ -1067,6 +1067,7 @@ public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext
"DeveloperBoard" => Symbol.DeveloperBoard,
"FolderLink" => Symbol.FolderLink,
"WindowConsole" => Symbol.WindowConsole,
"HardDrive" => Symbol.HardDrive,
_ => Symbol.Settings
};
}

View File

@@ -129,11 +129,11 @@ Name: "startup"; Description: "{cm:StartupTaskDescription}"; GroupDescription: "
Name: "{app}\log"; Permissions: users-modify
[InstallDelete]
Type: files; Name: "{app}\LanMontainDesktop.exe"
Type: files; Name: "{app}\LanMontainDesktop.dll"
Type: files; Name: "{app}\LanMontainDesktop.deps.json"
Type: files; Name: "{app}\LanMontainDesktop.runtimeconfig.json"
Type: files; Name: "{app}\LanMontainDesktop.pdb"
Type: files; Name: "{app}\LanMountainDesktop.exe"
Type: files; Name: "{app}\LanMountainDesktop.dll"
Type: files; Name: "{app}\LanMountainDesktop.deps.json"
Type: files; Name: "{app}\LanMountainDesktop.runtimeconfig.json"
Type: files; Name: "{app}\LanMountainDesktop.pdb"
[Files]
Source: "{#PublishDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
@@ -391,10 +391,6 @@ begin
begin
Params := Params + ' /SILENT';
end;
if WizardVerySilent then
begin
Params := Params + ' /VERYSILENT';
end;
{ 重启安装程序并退出当前实例 }
if Exec(ExpandConstant('{srcexe}'), Params, '', SW_SHOWNORMAL, ewNoWait, ResultCode) then