From 081abeb688d9c764f93200c6e620b98befcc25e4 Mon Sep 17 00:00:00 2001 From: lincube Date: Thu, 19 Mar 2026 00:17:21 +0800 Subject: [PATCH] 0 6 7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 可移动存储组件 --- .codex/environments/environment.toml | 16 + .../LanMountainDesktop.Tests.csproj | 1 + .../ComponentSystem/BuiltInComponentIds.cs | 1 + .../ComponentSystem/ComponentRegistry.cs | 9 + LanMountainDesktop/LanMountainDesktop.csproj | 4 + LanMountainDesktop/Localization/en-US.json | 17 + LanMountainDesktop/Localization/zh-CN.json | 17 + .../Models/AppSettingsSnapshot.cs | 2 + .../DesktopComponentEditorRegistryFactory.cs | 3 + .../Services/OfficeRecentDocumentsService.cs | 749 +++++++++++++----- .../Services/RemovableStorageService.cs | 310 ++++++++ .../Services/Settings/SettingsContracts.cs | 1 + .../Settings/SettingsDomainServices.cs | 3 + .../ComponentEditorThemeResources.axaml | 17 +- .../StatusBarSettingsPageViewModel.cs | 23 + .../Views/ComponentEditorWindow.axaml.cs | 1 + .../ClassScheduleComponentEditor.axaml | 19 +- .../ClassScheduleComponentEditor.axaml.cs | 53 +- .../RemovableStorageComponentEditor.axaml | 50 ++ .../RemovableStorageComponentEditor.axaml.cs | 67 ++ .../StudyEnvironmentComponentEditor.axaml | 23 +- .../StudyEnvironmentComponentEditor.axaml.cs | 47 +- .../Views/Components/ClockWidget.axaml.cs | 58 +- .../DesktopComponentRuntimeRegistry.cs | 5 + .../OfficeRecentDocumentsWidget.axaml.cs | 43 +- .../Components/RemovableStorageWidget.axaml | 119 +++ .../RemovableStorageWidget.axaml.cs | 596 ++++++++++++++ .../Views/MainWindow.ComponentSystem.cs | 3 + .../Views/MainWindow.SettingsHardCut.Stubs.cs | 1 + LanMountainDesktop/Views/MainWindow.axaml.cs | 1 + .../SettingsPages/StatusBarSettingsPage.axaml | 14 + testicon/testicon.csproj | 1 + 32 files changed, 2009 insertions(+), 265 deletions(-) create mode 100644 .codex/environments/environment.toml create mode 100644 LanMountainDesktop/Services/RemovableStorageService.cs create mode 100644 LanMountainDesktop/Views/ComponentEditors/RemovableStorageComponentEditor.axaml create mode 100644 LanMountainDesktop/Views/ComponentEditors/RemovableStorageComponentEditor.axaml.cs create mode 100644 LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml create mode 100644 LanMountainDesktop/Views/Components/RemovableStorageWidget.axaml.cs diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml new file mode 100644 index 0000000..40afd11 --- /dev/null +++ b/.codex/environments/environment.toml @@ -0,0 +1,16 @@ +# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY +version = 1 +name = "LanMountainDesktop" + +[setup] +script = "" + +[[actions]] +name = "运行" +icon = "run" +command = "dotnet run --project 'C:\\Users\\USER693091\\Documents\\GitHub\\LanMountainDesktop\\LanMountainDesktop\\LanMountainDesktop.csproj" + +[[actions]] +name = "构建" +icon = "tool" +command = "dotnet build 'C:\\Users\\USER693091\\Documents\\GitHub\\LanMountainDesktop\\LanMountainDesktop.slnx" diff --git a/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj b/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj index da890d9..d44230f 100644 --- a/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj +++ b/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj @@ -4,6 +4,7 @@ enable enable false + 1.0.0 diff --git a/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs b/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs index ab7a099..825c60f 100644 --- a/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs +++ b/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs @@ -41,4 +41,5 @@ public static class BuiltInComponentIds public const string DesktopBlackboardLandscape = "DesktopBlackboardLandscape"; public const string DesktopBrowser = "DesktopBrowser"; public const string DesktopOfficeRecentDocuments = "DesktopOfficeRecentDocuments"; + public const string DesktopRemovableStorage = "DesktopRemovableStorage"; } diff --git a/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs b/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs index 6c62cab..52c9c02 100644 --- a/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs +++ b/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs @@ -336,6 +336,15 @@ public sealed class ComponentRegistry MinHeightCells: 2, AllowStatusBarPlacement: false, AllowDesktopPlacement: true), + new DesktopComponentDefinition( + BuiltInComponentIds.DesktopRemovableStorage, + "Removable Storage", + "Storage", + "File", + MinWidthCells: 2, + MinHeightCells: 2, + AllowStatusBarPlacement: false, + AllowDesktopPlacement: true), new DesktopComponentDefinition( BuiltInComponentIds.Date, "Calendar", diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj index c369c61..06e054d 100644 --- a/LanMountainDesktop/LanMountainDesktop.csproj +++ b/LanMountainDesktop/LanMountainDesktop.csproj @@ -55,6 +55,10 @@ + + + + diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json index 8a4f8b9..be74c03 100644 --- a/LanMountainDesktop/Localization/en-US.json +++ b/LanMountainDesktop/Localization/en-US.json @@ -86,6 +86,8 @@ "settings.status_bar.description": "Choose which components appear on the top status bar.", "settings.status_bar.clock_header": "Clock Component", "settings.status_bar.clock_description": "Display a clock on the top status bar.", + "settings.status_bar.clock_transparent_background_label": "Transparent background", + "settings.status_bar.clock_transparent_background_desc": "Remove the capsule background and keep only the clock text.", "settings.status_bar.spacing_header": "Component Spacing", "settings.status_bar.spacing_desc": "Adjust spacing between status bar components.", "settings.status_bar.spacing_mode_compact": "Compact", @@ -588,6 +590,7 @@ "component.blackboard_landscape": "Blackboard (Landscape)", "component.browser": "Browser", "component.office_recent_documents": "Recent Documents", + "component.removable_storage": "Removable Storage", "component.holiday_calendar": "Holiday Calendar", "component.study_environment": "Environment", "component.study_session_control": "Study Session Control", @@ -789,6 +792,20 @@ "study.environment.settings.show_display_db": "Show display dB", "study.environment.settings.show_dbfs": "Show dBFS", "study.environment.settings.hint": "At least one display mode must stay enabled.", + "removable_storage.settings.desc": "Show a connected USB drive with quick open and eject actions.", + "removable_storage.settings.behavior_title": "Behavior", + "removable_storage.settings.behavior_desc": "The widget automatically watches for removable drives and switches to the newest inserted USB drive.", + "removable_storage.action.open": "Open", + "removable_storage.action.eject": "Eject", + "removable_storage.widget.default_name": "Removable Drive", + "removable_storage.widget.empty_title": "No device inserted", + "removable_storage.widget.empty_subtitle": "Insert a USB drive to show it here.", + "removable_storage.widget.empty_hint": "Buttons stay disabled until a removable device is inserted.", + "removable_storage.widget.ready": "Ready to open or eject.", + "removable_storage.widget.ejecting": "Ejecting drive...", + "removable_storage.widget.eject_failed": "Could not eject this drive. Close any files on it and try again.", + "removable_storage.widget.open_failed": "Failed to open this drive.", + "removable_storage.widget.refresh_failed": "Drive list refresh failed.", "study.session_control.action.start": "Start Study Session", "study.session_control.action.stop": "Stop Study Session", "study.session_control.idle_hint": "Tap the right button to start", diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json index 085c7c1..77a5c12 100644 --- a/LanMountainDesktop/Localization/zh-CN.json +++ b/LanMountainDesktop/Localization/zh-CN.json @@ -85,6 +85,8 @@ "settings.status_bar.description": "选择顶部状态栏显示的组件。", "settings.status_bar.clock_header": "时间组件", "settings.status_bar.clock_description": "在顶部状态栏显示时钟。", + "settings.status_bar.clock_transparent_background_label": "透明背景", + "settings.status_bar.clock_transparent_background_desc": "移除胶囊背景,仅保留时钟文字。", "settings.status_bar.spacing_header": "组件间距", "settings.status_bar.spacing_desc": "调整状态栏组件之间的间距。", "settings.status_bar.spacing_mode_compact": "紧凑", @@ -782,6 +784,21 @@ "study.environment.value.unavailable": "--", "study.environment.value.display_format": "{0:F1} dB", "study.environment.value.dbfs_format": "{0:F1} dBFS", + "component.removable_storage": "可移动存储", + "removable_storage.settings.desc": "在桌面上显示已连接的 U 盘,并提供打开与弹出操作。", + "removable_storage.settings.behavior_title": "行为", + "removable_storage.settings.behavior_desc": "组件会自动监听可移动存储设备,并优先显示最新插入的 U 盘。", + "removable_storage.action.open": "打开", + "removable_storage.action.eject": "弹出", + "removable_storage.widget.default_name": "可移动磁盘", + "removable_storage.widget.empty_title": "未插入设备", + "removable_storage.widget.empty_subtitle": "插入 U 盘后会自动显示在这里。", + "removable_storage.widget.empty_hint": "在插入可移动设备之前,底部按钮会保持置灰不可点击。", + "removable_storage.widget.ready": "已准备好,可直接打开或弹出。", + "removable_storage.widget.ejecting": "正在弹出设备...", + "removable_storage.widget.eject_failed": "无法弹出该设备,请先关闭正在占用它的文件后再试。", + "removable_storage.widget.open_failed": "打开该设备失败。", + "removable_storage.widget.refresh_failed": "刷新可移动存储列表失败。", "study.environment.settings.title": "环境组件设置", "study.environment.settings.desc": "配置右侧实时噪音值显示内容。", "study.environment.settings.show_display_db": "显示 display dB", diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs index 3880637..f28c8f9 100644 --- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs @@ -101,6 +101,8 @@ public sealed class AppSettingsSnapshot public string ClockDisplayFormat { get; set; } = "HourMinuteSecond"; + public bool StatusBarClockTransparentBackground { get; set; } + public string StatusBarSpacingMode { get; set; } = "Relaxed"; public int StatusBarCustomSpacingPercent { get; set; } = 12; diff --git a/LanMountainDesktop/Services/DesktopComponentEditorRegistryFactory.cs b/LanMountainDesktop/Services/DesktopComponentEditorRegistryFactory.cs index a4eb18f..212a539 100644 --- a/LanMountainDesktop/Services/DesktopComponentEditorRegistryFactory.cs +++ b/LanMountainDesktop/Services/DesktopComponentEditorRegistryFactory.cs @@ -72,6 +72,9 @@ public static class DesktopComponentEditorRegistryFactory [BuiltInComponentIds.DesktopStudyEnvironment] = new( BuiltInComponentIds.DesktopStudyEnvironment, context => new StudyEnvironmentComponentEditor(context)), + [BuiltInComponentIds.DesktopRemovableStorage] = new( + BuiltInComponentIds.DesktopRemovableStorage, + context => new RemovableStorageComponentEditor(context)), [BuiltInComponentIds.DesktopWeather] = CreateWeatherRegistration(BuiltInComponentIds.DesktopWeather), [BuiltInComponentIds.DesktopWeatherClock] = CreateWeatherRegistration(BuiltInComponentIds.DesktopWeatherClock), [BuiltInComponentIds.DesktopHourlyWeather] = CreateWeatherRegistration(BuiltInComponentIds.DesktopHourlyWeather), diff --git a/LanMountainDesktop/Services/OfficeRecentDocumentsService.cs b/LanMountainDesktop/Services/OfficeRecentDocumentsService.cs index 5a5394b..8900485 100644 --- a/LanMountainDesktop/Services/OfficeRecentDocumentsService.cs +++ b/LanMountainDesktop/Services/OfficeRecentDocumentsService.cs @@ -1,13 +1,18 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Text; -using System.Text.Json; -using LanMountainDesktop.Services.Settings; +using System.Text.RegularExpressions; +using System.Threading; using Microsoft.Win32; +using MudTools.OfficeInterop; +using MudTools.OfficeInterop.Excel; +using MudTools.OfficeInterop.Word; namespace LanMountainDesktop.Services; @@ -25,31 +30,49 @@ public sealed class OfficeRecentDocument public DateTime LastModifiedTime { get; set; } public long FileSizeBytes { get; set; } public string IconGlyph { get; set; } = string.Empty; + internal DateTime? RecentAccessTime { get; set; } + internal int SourcePriority { get; set; } + internal int SourceOrder { get; set; } } public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService { + private const string LogCategory = "OfficeRecentDocs"; private static readonly string[] OfficeExtensions = { ".doc", ".docx", ".dot", ".dotx", ".rtf" }; private static readonly string[] ExcelExtensions = { ".xls", ".xlsx", ".xlsm", ".xlsb", ".csv" }; private static readonly string[] PowerPointExtensions = { ".ppt", ".pptx", ".pptm", ".pps", ".ppsx" }; + private static readonly Regex OfficeFilePathRegex = new( + @"(?:[A-Z]:\\|\\\\)[^\x00-\x1F""<>|]+?\.(?:docx?|dotx?|rtf|xlsx?|xlsm|xlsb|csv|pptx?|pptm|ppsx?)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex OfficeMruTimestampRegex = new( + @"\[T(?[0-9A-F]+)\]", + RegexOptions.IgnoreCase | RegexOptions.Compiled); public List GetRecentDocuments(int maxCount = 20) { var documents = new List(); - // 方法1: 从注册表读取Office最近文档(最可靠) + if (!OperatingSystem.IsWindows()) + { + return documents; + } + TryGetFromRegistry(documents); - - // 方法2: 从Recent文件夹读取快捷方式(备用) TryGetFromRecentFolders(documents); + TryGetFromJumpLists(documents); - // 方法3: 从Windows Jump List读取(如果可用) - TryGetFromJumpList(documents); + if (documents.Count < maxCount) + { + TryGetFromMudToolsInterop(documents); + } return documents .GroupBy(d => d.FilePath, StringComparer.OrdinalIgnoreCase) - .Select(g => g.OrderByDescending(d => d.LastModifiedTime).First()) - .OrderByDescending(d => d.LastModifiedTime) + .Select(MergeDocuments) + .OrderByDescending(d => d.RecentAccessTime ?? DateTime.MinValue) + .ThenBy(d => d.SourcePriority) + .ThenBy(d => d.SourceOrder) + .ThenByDescending(d => d.LastModifiedTime) .Take(maxCount) .ToList(); } @@ -63,261 +86,587 @@ public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService FileName = filePath, UseShellExecute = true }; + Process.Start(startInfo); } + catch (Exception ex) + { + AppLogger.Warn(LogCategory, $"Failed to open Office document '{filePath}'.", ex); + } + } + + private static OfficeRecentDocument MergeDocuments(IGrouping group) + { + var preferred = group + .OrderByDescending(d => d.RecentAccessTime ?? DateTime.MinValue) + .ThenBy(d => d.SourcePriority) + .ThenBy(d => d.SourceOrder) + .ThenByDescending(d => d.LastModifiedTime) + .First(); + + return new OfficeRecentDocument + { + FileName = preferred.FileName, + FilePath = preferred.FilePath, + Extension = preferred.Extension, + LastModifiedTime = group.Max(d => d.LastModifiedTime), + FileSizeBytes = preferred.FileSizeBytes, + IconGlyph = preferred.IconGlyph, + RecentAccessTime = group + .Where(d => d.RecentAccessTime.HasValue) + .Select(d => d.RecentAccessTime) + .Max(), + SourcePriority = preferred.SourcePriority, + SourceOrder = preferred.SourceOrder + }; + } + + [SupportedOSPlatform("windows")] + private void TryGetFromMudToolsInterop(List documents) + { + try + { + RunOnStaThread(() => + { + var sourceOrder = 0; + TryGetFromWordInterop(documents, ref sourceOrder); + TryGetFromExcelInterop(documents, ref sourceOrder); + }); + } + catch (Exception ex) + { + AppLogger.Warn(LogCategory, "MudTools.OfficeInterop recent-document read failed.", ex); + } + } + + [SupportedOSPlatform("windows")] + private void TryGetFromWordInterop(List documents, ref int sourceOrder) + { + if (!TryGetOfficeApplication("Word.Application", out var comObject, out var createdNew)) + { + return; + } + + object? application = null; + + try + { + application = WordFactory.Connection(comObject!); + + if (createdNew) + { + TrySetProperty(comObject, "Visible", false); + TrySetProperty(application, "DisplayAlerts", WdAlertLevel.wdAlertsNone); + } + + AddInteropRecentFiles(documents, GetPropertyValue(application, "RecentFiles"), 0, ref sourceOrder); + } + catch (Exception ex) + { + AppLogger.Warn(LogCategory, "Failed to read Word recent files via MudTools.OfficeInterop.", ex); + } + finally + { + CleanupOfficeApplication(application, comObject, createdNew); + } + } + + [SupportedOSPlatform("windows")] + private void TryGetFromExcelInterop(List documents, ref int sourceOrder) + { + if (!TryGetOfficeApplication("Excel.Application", out var comObject, out var createdNew)) + { + return; + } + + object? application = null; + + try + { + application = ExcelFactory.Connection(comObject!); + + if (createdNew) + { + TrySetProperty(comObject, "Visible", false); + TrySetProperty(application, "DisplayAlerts", false); + } + + AddInteropRecentFiles(documents, GetPropertyValue(application, "RecentFiles"), 0, ref sourceOrder); + } + catch (Exception ex) + { + AppLogger.Warn(LogCategory, "Failed to read Excel recent files via MudTools.OfficeInterop.", ex); + } + finally + { + CleanupOfficeApplication(application, comObject, createdNew); + } + } + + private void AddInteropRecentFiles( + List documents, + object? recentFiles, + int sourcePriority, + ref int sourceOrder) + { + if (recentFiles == null) + { + return; + } + + var count = GetIntProperty(recentFiles, "Count"); + var itemProperty = recentFiles.GetType().GetProperty("Item"); + if (count <= 0 || itemProperty == null) + { + return; + } + + for (var index = 1; index <= count; index++) + { + try + { + var recentFile = itemProperty.GetValue(recentFiles, new object[] { index }); + var filePath = GetStringProperty(recentFile, "Path"); + AddDocumentIfExists(documents, filePath, sourcePriority, sourceOrder++, null); + } + catch + { + // Ignore a single malformed MRU entry and keep processing the rest. + } + } + } + + [SupportedOSPlatform("windows")] + private static bool TryGetOfficeApplication(string progId, out object? comObject, out bool createdNew) + { + comObject = null; + createdNew = false; + + var applicationType = Type.GetTypeFromProgID(progId, throwOnError: false); + if (applicationType == null) + { + return false; + } + + try + { + comObject = Activator.CreateInstance(applicationType); + createdNew = comObject != null; + return comObject != null; + } + catch + { + return false; + } + } + + [SupportedOSPlatform("windows")] + private static void CleanupOfficeApplication(object? application, object? comObject, bool createdNew) + { + try + { + if (createdNew && application != null) + { + InvokeParameterlessMethod(application, "Quit"); + } + } + catch + { + } + + try + { + if (application is IDisposable disposable) + { + disposable.Dispose(); + } + } + catch + { + } + + ReleaseComObject(application); + if (!ReferenceEquals(application, comObject)) + { + ReleaseComObject(comObject); + } + } + + [SupportedOSPlatform("windows")] + private static void ReleaseComObject(object? instance) + { + if (instance == null || !Marshal.IsComObject(instance)) + { + return; + } + + try + { + Marshal.FinalReleaseComObject(instance); + } catch { } } -#pragma warning disable CA1416 // 平台兼容性警告 + [SupportedOSPlatform("windows")] + private static void RunOnStaThread(Action action) + { + Exception? exception = null; + using var finished = new ManualResetEventSlim(); + + var thread = new Thread(() => + { + try + { + action(); + } + catch (Exception ex) + { + exception = ex; + } + finally + { + finished.Set(); + } + }); + + thread.IsBackground = true; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + finished.Wait(); + + if (exception != null) + { + throw new InvalidOperationException("Failed to run Office interop on STA thread.", exception); + } + } + + [SupportedOSPlatform("windows")] private void TryGetFromRegistry(List documents) { try { - // Word最近文档 - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\Word\Reading Locations"); - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Word\Reading Locations"); - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\15.0\Word\Reading Locations"); - - // Excel最近文档 - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\Excel\Reading Locations"); - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Excel\Reading Locations"); - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\15.0\Excel\Reading Locations"); - - // PowerPoint最近文档 - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\PowerPoint\Reading Locations"); - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\PowerPoint\Reading Locations"); - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\15.0\PowerPoint\Reading Locations"); - - // 通用Office最近文档 - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Common\Open Find\Microsoft Office Word"); - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Common\Open Find\Microsoft Office Excel"); - TryGetFromOfficeRegistry(documents, @"Software\Microsoft\Office\16.0\Common\Open Find\Microsoft Office PowerPoint"); - } - catch - { - // 忽略注册表访问错误 - } - } - - private void TryGetFromOfficeRegistry(List documents, string registryPath) - { - try - { - using var key = Registry.CurrentUser.OpenSubKey(registryPath); - if (key == null) return; - - foreach (var subKeyName in key.GetSubKeyNames()) + using var officeRoot = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Office"); + if (officeRoot == null) { - try - { - using var subKey = key.OpenSubKey(subKeyName); - if (subKey == null) continue; + return; + } - var filePath = subKey.GetValue("Path") as string; - if (string.IsNullOrEmpty(filePath)) continue; + var versions = officeRoot + .GetSubKeyNames() + .Where(IsOfficeVersionKey) + .OrderByDescending(ParseVersionKey) + .ToList(); - AddDocumentIfExists(documents, filePath); - } - catch - { - // 忽略单个子键访问错误 - } + var sourceOrder = 0; + foreach (var version in versions) + { + TryGetFromRegistryApp(documents, version, "Word", ref sourceOrder); + TryGetFromRegistryApp(documents, version, "Excel", ref sourceOrder); + TryGetFromRegistryApp(documents, version, "PowerPoint", ref sourceOrder); } } - catch + catch (Exception ex) { - // 忽略注册表访问错误 + AppLogger.Warn(LogCategory, "Failed to read Office MRU entries from the registry.", ex); + } + } + + [SupportedOSPlatform("windows")] + private void TryGetFromRegistryApp(List documents, string version, string appName, ref int sourceOrder) + { + TryGetFromRegistryMruKey(documents, $@"Software\Microsoft\Office\{version}\{appName}\File MRU", ref sourceOrder); + + using var userMruRoot = Registry.CurrentUser.OpenSubKey($@"Software\Microsoft\Office\{version}\{appName}\User MRU"); + if (userMruRoot == null) + { + return; + } + + foreach (var identityKey in userMruRoot.GetSubKeyNames()) + { + TryGetFromRegistryMruKey( + documents, + $@"Software\Microsoft\Office\{version}\{appName}\User MRU\{identityKey}\File MRU", + ref sourceOrder); + } + } + + [SupportedOSPlatform("windows")] + private void TryGetFromRegistryMruKey(List documents, string registryPath, ref int sourceOrder) + { + using var key = Registry.CurrentUser.OpenSubKey(registryPath); + if (key == null) + { + return; + } + + var entries = key + .GetValueNames() + .Where(name => name.StartsWith("Item ", StringComparison.OrdinalIgnoreCase)) + .Select(name => new + { + Name = name, + Order = ParseMruItemOrder(name), + Value = key.GetValue(name) as string + }) + .Where(entry => !string.IsNullOrWhiteSpace(entry.Value)) + .OrderBy(entry => entry.Order); + + foreach (var entry in entries) + { + var (filePath, recentAccessTime) = ParseOfficeMruValue(entry.Value!); + AddDocumentIfExists(documents, filePath, 1, sourceOrder++, recentAccessTime); } } -#pragma warning restore CA1416 // 平台兼容性警告 private void TryGetFromRecentFolders(List documents) { - var recentPaths = GetRecentFolders(); - - foreach (var recentPath in recentPaths) + try { - if (!Directory.Exists(recentPath)) - { - continue; - } + var linkFiles = GetRecentFolders() + .Where(Directory.Exists) + .SelectMany(path => Directory.EnumerateFiles(path, "*.lnk")) + .Select(path => new FileInfo(path)) + .OrderByDescending(info => info.LastWriteTimeUtc) + .ToList(); - try + var sourceOrder = 0; + foreach (var linkFile in linkFiles) { - var files = Directory.GetFiles(recentPath, "*.lnk"); - foreach (var lnkPath in files) - { - var targetPath = GetShortcutTarget(lnkPath); - if (string.IsNullOrEmpty(targetPath)) - { - continue; - } - - AddDocumentIfExists(documents, targetPath); - } - } - catch - { - // 忽略文件夹访问错误 + var targetPath = GetShortcutTarget(linkFile.FullName); + AddDocumentIfExists(documents, targetPath, 2, sourceOrder++, linkFile.LastWriteTime); } } + catch (Exception ex) + { + AppLogger.Warn(LogCategory, "Failed to read Windows Recent shortcut folders.", ex); + } } - private void TryGetFromJumpList(List documents) + private void TryGetFromJumpLists(List documents) { try { - // Windows Jump List存储在以下位置 - var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - var jumpListPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "Microsoft", "Windows", "Recent", "AutomaticDestinations"); + var jumpListFiles = GetJumpListFolders() + .Where(Directory.Exists) + .SelectMany(path => Directory.EnumerateFiles(path, "*.automaticDestinations-ms") + .Concat(Directory.EnumerateFiles(path, "*.customDestinations-ms"))) + .Select(path => new FileInfo(path)) + .OrderByDescending(info => info.LastWriteTimeUtc) + .ToList(); - if (!Directory.Exists(jumpListPath)) return; - - // Office应用的Jump List文件 - var officeJumpListFiles = new[] + var sourceOrder = 0; + foreach (var jumpListFile in jumpListFiles) { - "a7bd7a3f3d5a4c74.automaticDestinations-ms", // Word - "9b524fe3be704a4d.automaticDestinations-ms", // Excel - "d0063c4c7de64e5e.automaticDestinations-ms" // PowerPoint - }; + TryParseJumpListFile(jumpListFile, documents, ref sourceOrder); + } + } + catch (Exception ex) + { + AppLogger.Warn(LogCategory, "Failed to read Windows Jump Lists for Office documents.", ex); + } + } - foreach (var jumpFile in officeJumpListFiles) + private void TryParseJumpListFile(FileInfo jumpListFile, List documents, ref int sourceOrder) + { + try + { + var bytes = File.ReadAllBytes(jumpListFile.FullName); + foreach (var filePath in ExtractPossiblePaths(bytes)) { - var fullPath = Path.Combine(jumpListPath, jumpFile); - if (File.Exists(fullPath)) - { - TryParseJumpListFile(fullPath, documents); - } + AddDocumentIfExists(documents, filePath, 3, sourceOrder++, jumpListFile.LastWriteTime); } } catch { - // Jump List解析失败,忽略 + // Ignore a single Jump List file and keep scanning the rest. } } - private void TryParseJumpListFile(string jumpListPath, List documents) + private static IEnumerable ExtractPossiblePaths(byte[] bytes) { - try - { - // Jump List文件是二进制格式,这里使用简化的方法 - // 读取文件并尝试提取文件路径 - var bytes = File.ReadAllBytes(jumpListPath); - var text = Encoding.Unicode.GetString(bytes); + var paths = new HashSet(StringComparer.OrdinalIgnoreCase); - // 查找可能的文件路径(简化实现) - var possiblePaths = ExtractPossiblePaths(text); - foreach (var path in possiblePaths) + foreach (var text in new[] + { + Encoding.Unicode.GetString(bytes), + Encoding.Latin1.GetString(bytes) + }) + { + foreach (Match match in OfficeFilePathRegex.Matches(text)) { - AddDocumentIfExists(documents, path); - } - } - catch - { - // Jump List解析失败,忽略 - } - } - - private IEnumerable ExtractPossiblePaths(string text) - { - var paths = new List(); - - // 查找常见的文件路径模式 - var patterns = new[] - { - @"[A-Z]:\\[^\x00-\x1F""<>|]*\.(docx?|xlsx?|pptx?|rtf|csv)", - @"\\\\[^\\]+\\[^\x00-\x1F""<>|]*\.(docx?|xlsx?|pptx?|rtf|csv)" - }; - - foreach (var pattern in patterns) - { - try - { - var matches = System.Text.RegularExpressions.Regex.Matches(text, pattern, - System.Text.RegularExpressions.RegexOptions.IgnoreCase); - - foreach (System.Text.RegularExpressions.Match match in matches) + var normalizedPath = NormalizeFilePath(match.Value); + if (!string.IsNullOrWhiteSpace(normalizedPath)) { - var path = match.Value.Trim('\0', ' ', '"'); - if (!string.IsNullOrEmpty(path)) - { - paths.Add(path); - } + paths.Add(normalizedPath); } } - catch - { - // 忽略正则表达式错误 - } } - return paths.Distinct(StringComparer.OrdinalIgnoreCase); + return paths; } - private void AddDocumentIfExists(List documents, string filePath) + private void AddDocumentIfExists( + List documents, + string? filePath, + int sourcePriority, + int sourceOrder, + DateTime? recentAccessTime) { try { - var extension = Path.GetExtension(filePath).ToLowerInvariant(); - if (!IsOfficeFile(extension)) + var normalizedPath = NormalizeFilePath(filePath); + if (string.IsNullOrWhiteSpace(normalizedPath)) { return; } - if (!File.Exists(filePath)) + var extension = Path.GetExtension(normalizedPath).ToLowerInvariant(); + if (!IsOfficeFile(extension) || !File.Exists(normalizedPath)) { return; } - var fileInfo = new FileInfo(filePath); - var doc = new OfficeRecentDocument + var fileInfo = new FileInfo(normalizedPath); + documents.Add(new OfficeRecentDocument { - FileName = Path.GetFileNameWithoutExtension(filePath), - FilePath = filePath, + FileName = Path.GetFileNameWithoutExtension(normalizedPath), + FilePath = normalizedPath, Extension = extension, LastModifiedTime = fileInfo.LastWriteTime, FileSizeBytes = fileInfo.Length, - IconGlyph = GetIconGlyph(extension) - }; - - if (!documents.Any(d => string.Equals(d.FilePath, filePath, StringComparison.OrdinalIgnoreCase))) - { - documents.Add(doc); - } + IconGlyph = GetIconGlyph(extension), + RecentAccessTime = recentAccessTime, + SourcePriority = sourcePriority, + SourceOrder = sourceOrder + }); } catch { - // 忽略单个文件处理错误 + // Ignore a single file and keep processing the rest of the MRU list. } } - private static List GetRecentFolders() + private static IEnumerable GetRecentFolders() { - var folders = new List(); - var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - folders.Add(Path.Combine(appData, "Microsoft", "Word", "Recent")); - folders.Add(Path.Combine(appData, "Microsoft", "Excel", "Recent")); - folders.Add(Path.Combine(appData, "Microsoft", "PowerPoint", "Recent")); - - // 添加Office 365路径 var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - folders.Add(Path.Combine(localAppData, "Microsoft", "Office", "Word", "Recent")); - folders.Add(Path.Combine(localAppData, "Microsoft", "Office", "Excel", "Recent")); - folders.Add(Path.Combine(localAppData, "Microsoft", "Office", "PowerPoint", "Recent")); - return folders; + return new[] + { + Path.Combine(appData, "Microsoft", "Windows", "Recent"), + Path.Combine(appData, "Microsoft", "Word", "Recent"), + Path.Combine(appData, "Microsoft", "Excel", "Recent"), + Path.Combine(appData, "Microsoft", "PowerPoint", "Recent"), + Path.Combine(localAppData, "Microsoft", "Office", "Word", "Recent"), + Path.Combine(localAppData, "Microsoft", "Office", "Excel", "Recent"), + Path.Combine(localAppData, "Microsoft", "Office", "PowerPoint", "Recent") + }.Distinct(StringComparer.OrdinalIgnoreCase); + } + + private static IEnumerable GetJumpListFolders() + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + return new[] + { + Path.Combine(appData, "Microsoft", "Windows", "Recent", "AutomaticDestinations"), + Path.Combine(appData, "Microsoft", "Windows", "Recent", "CustomDestinations"), + Path.Combine(localAppData, "Microsoft", "Windows", "Recent", "AutomaticDestinations"), + Path.Combine(localAppData, "Microsoft", "Windows", "Recent", "CustomDestinations") + }.Distinct(StringComparer.OrdinalIgnoreCase); + } + + private static bool IsOfficeVersionKey(string keyName) + { + return Version.TryParse(keyName, out _); + } + + private static Version ParseVersionKey(string keyName) + { + return Version.TryParse(keyName, out var version) ? version : new Version(0, 0); + } + + private static int ParseMruItemOrder(string valueName) + { + var numberText = valueName["Item ".Length..]; + return int.TryParse(numberText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var number) + ? number + : int.MaxValue; + } + + private static (string? FilePath, DateTime? RecentAccessTime) ParseOfficeMruValue(string rawValue) + { + var filePath = ExtractOfficeFilePath(rawValue); + DateTime? recentAccessTime = null; + + var timestampMatch = OfficeMruTimestampRegex.Match(rawValue); + if (timestampMatch.Success && + long.TryParse(timestampMatch.Groups["filetime"].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var fileTime) && + fileTime > 0) + { + try + { + recentAccessTime = DateTime.FromFileTimeUtc(fileTime).ToLocalTime(); + } + catch + { + recentAccessTime = null; + } + } + + return (filePath, recentAccessTime); + } + + private static string? ExtractOfficeFilePath(string rawValue) + { + if (string.IsNullOrWhiteSpace(rawValue)) + { + return null; + } + + var markerIndex = rawValue.LastIndexOf('*'); + var candidate = markerIndex >= 0 + ? rawValue[(markerIndex + 1)..] + : rawValue; + + var normalizedCandidate = NormalizeFilePath(candidate); + if (!string.IsNullOrWhiteSpace(normalizedCandidate) && IsOfficeFile(Path.GetExtension(normalizedCandidate))) + { + return normalizedCandidate; + } + + var match = OfficeFilePathRegex.Match(rawValue); + return match.Success ? NormalizeFilePath(match.Value) : null; + } + + private static string? NormalizeFilePath(string? rawPath) + { + if (string.IsNullOrWhiteSpace(rawPath)) + { + return null; + } + + var candidate = rawPath.Trim('\0', ' ', '"'); + candidate = Environment.ExpandEnvironmentVariables(candidate); + + if (Uri.TryCreate(candidate, UriKind.Absolute, out var uri) && uri.IsFile) + { + candidate = uri.LocalPath; + } + + candidate = candidate.Replace('/', '\\'); + return string.IsNullOrWhiteSpace(candidate) ? null : candidate; } private static bool IsOfficeFile(string extension) { - return OfficeExtensions.Contains(extension) || - ExcelExtensions.Contains(extension) || - PowerPointExtensions.Contains(extension); + return OfficeExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) || + ExcelExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) || + PowerPointExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase); } private static string GetIconGlyph(string extension) @@ -335,4 +684,40 @@ public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService { return ShortcutHelper.GetShortcutTarget(lnkPath); } + + private static object? GetPropertyValue(object? instance, string propertyName) + { + return instance?.GetType().GetProperty(propertyName)?.GetValue(instance); + } + + private static string? GetStringProperty(object? instance, string propertyName) + { + return GetPropertyValue(instance, propertyName) as string; + } + + private static int GetIntProperty(object instance, string propertyName) + { + var value = GetPropertyValue(instance, propertyName); + return value switch + { + int intValue => intValue, + short shortValue => shortValue, + long longValue => (int)longValue, + _ => 0 + }; + } + + private static void TrySetProperty(object? instance, string propertyName, object value) + { + var property = instance?.GetType().GetProperty(propertyName); + if (property?.CanWrite == true) + { + property.SetValue(instance, value); + } + } + + private static void InvokeParameterlessMethod(object instance, string methodName) + { + instance.GetType().GetMethod(methodName, Type.EmptyTypes)?.Invoke(instance, null); + } } diff --git a/LanMountainDesktop/Services/RemovableStorageService.cs b/LanMountainDesktop/Services/RemovableStorageService.cs new file mode 100644 index 0000000..726e5f3 --- /dev/null +++ b/LanMountainDesktop/Services/RemovableStorageService.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace LanMountainDesktop.Services; + +public sealed record RemovableStorageDrive( + string RootPath, + string DriveLetter, + string? VolumeLabel); + +public interface IRemovableStorageService +{ + IReadOnlyList GetConnectedDrives(); + + bool OpenDrive(string rootPath); + + bool EjectDrive(string rootPath); +} + +public sealed class RemovableStorageService : IRemovableStorageService +{ + public IReadOnlyList GetConnectedDrives() + { + var drives = new List(); + + foreach (var drive in DriveInfo.GetDrives()) + { + try + { + if (drive.DriveType != DriveType.Removable || !drive.IsReady) + { + continue; + } + + var rootPath = NormalizeRootPath(drive.Name); + if (string.IsNullOrWhiteSpace(rootPath)) + { + continue; + } + + var driveLetter = rootPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var volumeLabel = string.IsNullOrWhiteSpace(drive.VolumeLabel) + ? null + : drive.VolumeLabel.Trim(); + + drives.Add(new RemovableStorageDrive(rootPath, driveLetter, volumeLabel)); + } + catch (Exception ex) + { + AppLogger.Warn("RemovableStorage", $"Failed to inspect drive '{drive.Name}'.", ex); + } + } + + return drives + .OrderBy(drive => drive.DriveLetter, StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + public bool OpenDrive(string rootPath) + { + var normalizedRootPath = NormalizeRootPath(rootPath); + if (string.IsNullOrWhiteSpace(normalizedRootPath)) + { + return false; + } + + try + { + Process.Start(new ProcessStartInfo + { + FileName = normalizedRootPath, + UseShellExecute = true + }); + return true; + } + catch (Exception ex) + { + AppLogger.Warn("RemovableStorage", $"Failed to open drive '{normalizedRootPath}'.", ex); + return false; + } + } + + public bool EjectDrive(string rootPath) + { + if (!OperatingSystem.IsWindows()) + { + return false; + } + + var normalizedRootPath = NormalizeRootPath(rootPath); + if (string.IsNullOrWhiteSpace(normalizedRootPath)) + { + return false; + } + + object? shellApplication = null; + object? computerFolder = null; + object? driveItem = null; + + try + { + var shellType = Type.GetTypeFromProgID("Shell.Application"); + if (shellType is null) + { + return false; + } + + shellApplication = Activator.CreateInstance(shellType); + if (shellApplication is null) + { + return false; + } + + computerFolder = shellType.InvokeMember( + "NameSpace", + BindingFlags.InvokeMethod, + binder: null, + target: shellApplication, + args: [17]); + if (computerFolder is null) + { + return false; + } + + var driveToken = normalizedRootPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + driveItem = computerFolder.GetType().InvokeMember( + "ParseName", + BindingFlags.InvokeMethod, + binder: null, + target: computerFolder, + args: [driveToken]); + if (driveItem is null) + { + return false; + } + + if (TryInvokeVerb(driveItem, "Eject")) + { + return true; + } + + return TryInvokeLocalizedEjectVerb(driveItem); + } + catch (Exception ex) + { + AppLogger.Warn("RemovableStorage", $"Failed to eject drive '{normalizedRootPath}'.", ex); + return false; + } + finally + { + ReleaseComObject(driveItem); + ReleaseComObject(computerFolder); + ReleaseComObject(shellApplication); + } + } + + private static bool TryInvokeLocalizedEjectVerb(object driveItem) + { + object? verbs = null; + + try + { + verbs = driveItem.GetType().InvokeMember( + "Verbs", + BindingFlags.InvokeMethod, + binder: null, + target: driveItem, + args: null); + if (verbs is null) + { + return false; + } + + var verbsType = verbs.GetType(); + var countObject = verbsType.InvokeMember( + "Count", + BindingFlags.GetProperty, + binder: null, + target: verbs, + args: null); + var count = countObject is null + ? 0 + : Convert.ToInt32(countObject, CultureInfo.InvariantCulture); + + for (var index = 0; index < count; index++) + { + object? verb = null; + + try + { + verb = verbsType.InvokeMember( + "Item", + BindingFlags.InvokeMethod, + binder: null, + target: verbs, + args: [index]); + if (verb is null) + { + continue; + } + + var verbNameObject = verb.GetType().InvokeMember( + "Name", + BindingFlags.GetProperty, + binder: null, + target: verb, + args: null); + var verbName = Convert.ToString(verbNameObject, CultureInfo.InvariantCulture); + if (!IsEjectVerbName(verbName)) + { + continue; + } + + verb.GetType().InvokeMember( + "DoIt", + BindingFlags.InvokeMethod, + binder: null, + target: verb, + args: null); + return true; + } + finally + { + ReleaseComObject(verb); + } + } + + return false; + } + finally + { + ReleaseComObject(verbs); + } + } + + private static bool TryInvokeVerb(object driveItem, string verbName) + { + try + { + driveItem.GetType().InvokeMember( + "InvokeVerb", + BindingFlags.InvokeMethod, + binder: null, + target: driveItem, + args: [verbName]); + return true; + } + catch + { + return false; + } + } + + private static bool IsEjectVerbName(string? verbName) + { + if (string.IsNullOrWhiteSpace(verbName)) + { + return false; + } + + var normalized = string.Concat( + verbName + .Where(character => !char.IsWhiteSpace(character) && character != '&')) + .Trim(); + + return normalized.Contains("Eject", StringComparison.OrdinalIgnoreCase) || + normalized.Contains("弹出", StringComparison.Ordinal) || + normalized.Contains("安全删除", StringComparison.Ordinal) || + normalized.Contains("卸载", StringComparison.Ordinal); + } + + private static string NormalizeRootPath(string? rootPath) + { + if (string.IsNullOrWhiteSpace(rootPath)) + { + return string.Empty; + } + + var trimmed = rootPath.Trim(); + if (trimmed.Length == 1 && char.IsLetter(trimmed[0])) + { + return string.Create(CultureInfo.InvariantCulture, $"{trimmed}:{Path.DirectorySeparatorChar}"); + } + + if (trimmed.Length == 2 && char.IsLetter(trimmed[0]) && trimmed[1] == ':') + { + return trimmed + Path.DirectorySeparatorChar; + } + + var normalized = trimmed.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + var resolvedRoot = Path.GetPathRoot(normalized); + return string.IsNullOrWhiteSpace(resolvedRoot) + ? normalized + : resolvedRoot; + } + + private static void ReleaseComObject(object? value) + { + if (value is not null && Marshal.IsComObject(value)) + { + Marshal.FinalReleaseComObject(value); + } + } +} diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs index 1dabba4..7a82e8f 100644 --- a/LanMountainDesktop/Services/Settings/SettingsContracts.cs +++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs @@ -29,6 +29,7 @@ public sealed record StatusBarSettingsState( bool EnableDynamicTaskbarActions, string TaskbarLayoutMode, string ClockDisplayFormat, + bool ClockTransparentBackground, string SpacingMode, int CustomSpacingPercent); public sealed record WeatherSettingsState( diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs index beca663..6ef48d2 100644 --- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs +++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs @@ -361,6 +361,7 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService snapshot.EnableDynamicTaskbarActions, snapshot.TaskbarLayoutMode, snapshot.ClockDisplayFormat, + snapshot.StatusBarClockTransparentBackground, snapshot.StatusBarSpacingMode, snapshot.StatusBarCustomSpacingPercent); } @@ -373,6 +374,7 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService snapshot.EnableDynamicTaskbarActions = state.EnableDynamicTaskbarActions; snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode; snapshot.ClockDisplayFormat = state.ClockDisplayFormat; + snapshot.StatusBarClockTransparentBackground = state.ClockTransparentBackground; snapshot.StatusBarSpacingMode = state.SpacingMode; snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent; _settingsService.SaveSnapshot( @@ -385,6 +387,7 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService nameof(AppSettingsSnapshot.EnableDynamicTaskbarActions), nameof(AppSettingsSnapshot.TaskbarLayoutMode), nameof(AppSettingsSnapshot.ClockDisplayFormat), + nameof(AppSettingsSnapshot.StatusBarClockTransparentBackground), nameof(AppSettingsSnapshot.StatusBarSpacingMode), nameof(AppSettingsSnapshot.StatusBarCustomSpacingPercent) ]); diff --git a/LanMountainDesktop/Styles/ComponentEditorThemeResources.axaml b/LanMountainDesktop/Styles/ComponentEditorThemeResources.axaml index 3fcbaf8..43e9588 100644 --- a/LanMountainDesktop/Styles/ComponentEditorThemeResources.axaml +++ b/LanMountainDesktop/Styles/ComponentEditorThemeResources.axaml @@ -1,5 +1,6 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:assists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles"> @@ -74,7 +75,21 @@ + + + +