mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-29 22:24:26 +08:00
0 6 7
可移动存储组件
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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(?<filetime>[0-9A-F]+)\]",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public List<OfficeRecentDocument> GetRecentDocuments(int maxCount = 20)
|
||||
{
|
||||
var documents = new List<OfficeRecentDocument>();
|
||||
|
||||
// 方法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<string, OfficeRecentDocument> 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<OfficeRecentDocument> 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<OfficeRecentDocument> 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<OfficeRecentDocument> 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<OfficeRecentDocument> 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<OfficeRecentDocument> 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<OfficeRecentDocument> 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<OfficeRecentDocument> 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<OfficeRecentDocument> 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<OfficeRecentDocument> 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<OfficeRecentDocument> documents)
|
||||
private void TryGetFromJumpLists(List<OfficeRecentDocument> 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<OfficeRecentDocument> 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<OfficeRecentDocument> documents)
|
||||
private static IEnumerable<string> ExtractPossiblePaths(byte[] bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Jump List文件是二进制格式,这里使用简化的方法
|
||||
// 读取文件并尝试提取文件路径
|
||||
var bytes = File.ReadAllBytes(jumpListPath);
|
||||
var text = Encoding.Unicode.GetString(bytes);
|
||||
var paths = new HashSet<string>(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<string> ExtractPossiblePaths(string text)
|
||||
{
|
||||
var paths = new List<string>();
|
||||
|
||||
// 查找常见的文件路径模式
|
||||
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<OfficeRecentDocument> documents, string filePath)
|
||||
private void AddDocumentIfExists(
|
||||
List<OfficeRecentDocument> 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<string> GetRecentFolders()
|
||||
private static IEnumerable<string> GetRecentFolders()
|
||||
{
|
||||
var folders = new List<string>();
|
||||
|
||||
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<string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
310
LanMountainDesktop/Services/RemovableStorageService.cs
Normal file
310
LanMountainDesktop/Services/RemovableStorageService.cs
Normal file
@@ -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<RemovableStorageDrive> GetConnectedDrives();
|
||||
|
||||
bool OpenDrive(string rootPath);
|
||||
|
||||
bool EjectDrive(string rootPath);
|
||||
}
|
||||
|
||||
public sealed class RemovableStorageService : IRemovableStorageService
|
||||
{
|
||||
public IReadOnlyList<RemovableStorageDrive> GetConnectedDrives()
|
||||
{
|
||||
var drives = new List<RemovableStorageDrive>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ public sealed record StatusBarSettingsState(
|
||||
bool EnableDynamicTaskbarActions,
|
||||
string TaskbarLayoutMode,
|
||||
string ClockDisplayFormat,
|
||||
bool ClockTransparentBackground,
|
||||
string SpacingMode,
|
||||
int CustomSpacingPercent);
|
||||
public sealed record WeatherSettingsState(
|
||||
|
||||
@@ -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)
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user