diff --git a/.cursor/skills/.gitkeep b/.cursor/skills/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.cursor/skills/.gitkeep @@ -0,0 +1 @@ + diff --git a/Directory.Packages.props b/Directory.Packages.props index aa56451..f43665b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,6 +16,7 @@ + diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj index 4af574e..464bcdd 100644 --- a/LanMountainDesktop/LanMountainDesktop.csproj +++ b/LanMountainDesktop/LanMountainDesktop.csproj @@ -56,6 +56,7 @@ + diff --git a/LanMountainDesktop/Platform/Windows/ChromePatchState.cs b/LanMountainDesktop/Platform/Windows/ChromePatchState.cs new file mode 100644 index 0000000..7ee09a6 --- /dev/null +++ b/LanMountainDesktop/Platform/Windows/ChromePatchState.cs @@ -0,0 +1,6 @@ +namespace LanMountainDesktop.Platform.Windows; + +internal static class ChromePatchState +{ + public static bool UseSystemChrome { get; set; } +} diff --git a/LanMountainDesktop/Platform/Windows/PatcherEntrance.cs b/LanMountainDesktop/Platform/Windows/PatcherEntrance.cs new file mode 100644 index 0000000..6cecf3d --- /dev/null +++ b/LanMountainDesktop/Platform/Windows/PatcherEntrance.cs @@ -0,0 +1,12 @@ +using HarmonyLib; + +namespace LanMountainDesktop.Platform.Windows; + +internal static class PatcherEntrance +{ + public static void InstallPatchers() + { + var harmony = new Harmony("dev.lanmountain.desktop.patchers"); + harmony.PatchAll(typeof(PatcherEntrance).Assembly); + } +} diff --git a/LanMountainDesktop/Platform/Windows/Patches/AppWindowInitializeAppWindowPatcher.cs b/LanMountainDesktop/Platform/Windows/Patches/AppWindowInitializeAppWindowPatcher.cs new file mode 100644 index 0000000..0941f4e --- /dev/null +++ b/LanMountainDesktop/Platform/Windows/Patches/AppWindowInitializeAppWindowPatcher.cs @@ -0,0 +1,25 @@ +using System.Runtime.CompilerServices; +using Avalonia; +using Avalonia.Controls; +using FluentAvalonia.UI.Windowing; +using LanMountainDesktop.Platform.Windows; +using HarmonyLib; + +namespace LanMountainDesktop.Platform.Windows.Patches; + +[HarmonyPatch(typeof(FAAppWindow), "InitializeAppWindow")] +internal class AppWindowInitializeAppWindowPatcher +{ + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_PseudoClasses")] + private static extern IPseudoClasses GetPseudoClasses(StyledElement window); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_IsWindows")] + private static extern void SetIsWindowsProperty(FAAppWindow window, bool v); + + static void Postfix(FAAppWindow __instance) + { + if (!ChromePatchState.UseSystemChrome) return; + GetPseudoClasses(__instance).Remove(":windows"); + SetIsWindowsProperty(__instance, false); + } +} diff --git a/LanMountainDesktop/Platform/Windows/Patches/Win32WindowManagerConstructorPatcher.cs b/LanMountainDesktop/Platform/Windows/Patches/Win32WindowManagerConstructorPatcher.cs new file mode 100644 index 0000000..e67b3f8 --- /dev/null +++ b/LanMountainDesktop/Platform/Windows/Patches/Win32WindowManagerConstructorPatcher.cs @@ -0,0 +1,21 @@ +using FluentAvalonia.UI.Windowing; +using HarmonyLib; +using LanMountainDesktop.Platform.Windows; + +namespace LanMountainDesktop.Platform.Windows.Patches; + +[HarmonyPatch] +internal class Win32WindowManagerConstructorPatcher +{ + [HarmonyTargetMethod] + static System.Reflection.MethodBase TargetMethod() + { + var type = AccessTools.TypeByName("FluentAvalonia.UI.Windowing.Win32WindowManager"); + return AccessTools.Constructor(type!, [typeof(FAAppWindow)]); + } + + static bool Prefix(FAAppWindow window) + { + return !ChromePatchState.UseSystemChrome; + } +} diff --git a/LanMountainDesktop/Program.cs b/LanMountainDesktop/Program.cs index 1e1a50d..f911de9 100644 --- a/LanMountainDesktop/Program.cs +++ b/LanMountainDesktop/Program.cs @@ -87,6 +87,8 @@ public sealed class Program AppLogger.Info("SingleInstance", "Activation acknowledged before Avalonia App was ready."); }); + LoadChromePatchState(); + InstallChromePatchersIfNeeded(); BuildAvaloniaApp(renderMode).StartWithClassicDesktopLifetime(args); AppLogger.Info("Startup", "Application exited normally."); } @@ -198,6 +200,49 @@ public sealed class Program } } + private static void LoadChromePatchState() + { + try + { + var snapshot = HostSettingsFacadeProvider.GetOrCreate() + .Settings + .LoadSnapshot(LanMountainDesktop.PluginSdk.SettingsScope.App); + if (OperatingSystem.IsWindows()) + { + LanMountainDesktop.Platform.Windows.ChromePatchState.UseSystemChrome = snapshot.UseSystemChrome; + } + } + catch (Exception ex) + { + AppLogger.Warn("Startup", "Failed to load chrome patch state. Falling back to FA chrome.", ex); + } + } + + private static void InstallChromePatchersIfNeeded() + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + var arch = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture; + if (arch != System.Runtime.InteropServices.Architecture.X64 && + arch != System.Runtime.InteropServices.Architecture.X86) + { + return; + } + + try + { + LanMountainDesktop.Platform.Windows.PatcherEntrance.InstallPatchers(); + AppLogger.Info("Startup", $"Chrome patchers installed. UseSystemChrome={LanMountainDesktop.Platform.Windows.ChromePatchState.UseSystemChrome}."); + } + catch (Exception ex) + { + AppLogger.Warn("Startup", "Failed to install chrome patchers.", ex); + } + } + private static void WaitForRestartParentExit(int processId, DateTime deadlineUtc) { try diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs index e09021f..ff8255a 100644 --- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs +++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs @@ -31,12 +31,14 @@ public sealed partial class SettingsWindowViewModel : ViewModelBase { _localizationService = new(); _languageCode = "zh-CN"; + IsWindowsOs = OperatingSystem.IsWindows(); } public SettingsWindowViewModel(LocalizationService localizationService, string languageCode) { _localizationService = localizationService; _languageCode = languageCode; + IsWindowsOs = OperatingSystem.IsWindows(); } private string L(string key) => _localizationService.GetString(_languageCode, key, key); @@ -86,6 +88,10 @@ public sealed partial class SettingsWindowViewModel : ViewModelBase [ObservableProperty] private bool _isDrawerOpen; + /// 用于标题栏右侧系统按钮占位(与 SecRandom / ClassIsland 一致,仅 Windows 显示)。 + [ObservableProperty] + private bool _isWindowsOs; + public SettingsWindowViewModel Initialize() { RefreshLanguage(_languageCode); @@ -855,7 +861,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase return; } - PersistCurrentState(restartRequired: false); + PersistCurrentState(restartRequired: true); } partial void OnSelectedCornerRadiusStyleChanged(SelectionOption? value) diff --git a/LanMountainDesktop/Views/SettingsWindow.axaml b/LanMountainDesktop/Views/SettingsWindow.axaml index d95e0b2..9ca9764 100644 --- a/LanMountainDesktop/Views/SettingsWindow.axaml +++ b/LanMountainDesktop/Views/SettingsWindow.axaml @@ -1,26 +1,26 @@ - + - + 960 - + - + - + + - - - - - - + + + + + + - + PointerPressed="OnTitleBarDragZonePointerPressed" /> - + + + + + + + + + @@ -182,4 +195,4 @@ - + diff --git a/LanMountainDesktop/Views/SettingsWindow.axaml.cs b/LanMountainDesktop/Views/SettingsWindow.axaml.cs index 0608187..83f1e66 100644 --- a/LanMountainDesktop/Views/SettingsWindow.axaml.cs +++ b/LanMountainDesktop/Views/SettingsWindow.axaml.cs @@ -5,9 +5,10 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Input; -using Avalonia.Platform; +using Avalonia.Media; using Avalonia.Threading; using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Windowing; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; @@ -16,7 +17,7 @@ using Symbol = FluentIcons.Common.Symbol; namespace LanMountainDesktop.Views; -public partial class SettingsWindow : Window, ISettingsPageHostContext +public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext { private const double BaseSettingsContainerWidth = 960d; private const double MinSettingsContentWidth = 320d; @@ -56,7 +57,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext _hostApplicationLifecycle = hostApplicationLifecycle; DataContext = ViewModel; InitializeComponent(); - Icon = _appLogoService.CreateWindowIcon(); + SetValue(Window.IconProperty, _appLogoService.CreateWindowIcon()); ApplyChromeMode(useSystemChrome); if (RootNavigationView is not null) @@ -75,6 +76,14 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext private void OnLoaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) { + TitleBar.Height = 48; + TitleBar.ExtendsContentIntoTitleBar = true; + + // SecRandom MainWindow:标题栏按钮悬停/按下/非活动色,与系统 caption 更一致 + TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(23, 0, 0, 0); + TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(52, 0, 0, 0); + TitleBar.ButtonInactiveForegroundColor = Colors.Gray; + SyncPendingRestartState(); SyncTitleText(); UpdateChromeMetrics(); @@ -160,11 +169,11 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext { _useSystemChrome = useSystemChrome || OperatingSystem.IsMacOS(); + ExtendClientAreaToDecorationsHint = true; + WindowDecorations = WindowDecorations.Full; + if (_useSystemChrome) { - ExtendClientAreaToDecorationsHint = true; - WindowDecorations = WindowDecorations.Full; - if (WindowTitleBarHost is { }) { WindowTitleBarHost.IsVisible = false; @@ -172,9 +181,6 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext return; } - WindowDecorations = WindowDecorations.BorderOnly; - ExtendClientAreaToDecorationsHint = true; - if (WindowTitleBarHost is { }) { WindowTitleBarHost.IsVisible = true; @@ -195,21 +201,32 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext } RootNavigationView.MenuItems.Clear(); + RootNavigationView.FooterMenuItems.Clear(); + SettingsPageCategory? previousCategory = null; foreach (var page in ViewModel.Pages) { + var item = new FANavigationViewItem + { + Content = page.Title, + Tag = page.PageId, + IconSource = CreateSettingsIconSource(MapIcon(page.IconKey)) + }; + + if (page.Category == SettingsPageCategory.About || + page.Category == SettingsPageCategory.Dev) + { + RootNavigationView.FooterMenuItems.Add(item); + continue; + } + if (previousCategory is not null && previousCategory != page.Category) { RootNavigationView.MenuItems.Add(new FANavigationViewItemSeparator()); } - RootNavigationView.MenuItems.Add(new FANavigationViewItem - { - Content = page.Title, - Tag = page.PageId, - IconSource = CreateSettingsIconSource(MapIcon(page.IconKey)) - }); + RootNavigationView.MenuItems.Add(item); previousCategory = page.Category; } @@ -293,7 +310,10 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext return; } - foreach (var item in RootNavigationView.MenuItems.OfType()) + var allItems = RootNavigationView.MenuItems.OfType() + .Concat(RootNavigationView.FooterMenuItems.OfType()); + + foreach (var item in allItems) { if (string.Equals(item.Tag as string, pageId, StringComparison.OrdinalIgnoreCase)) { @@ -494,7 +514,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext TelemetryServices.Usage?.TrackSettingsWindowClosed("SettingsWindow.OnClosed", ViewModel.CurrentPageId); } - private void OnWindowTitleBarPointerPressed(object? sender, PointerPressedEventArgs e) + private void OnTitleBarDragZonePointerPressed(object? sender, PointerPressedEventArgs e) { _ = sender; if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) @@ -518,13 +538,6 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext RequestResponsiveLayoutRefresh(); } - private void OnCloseWindowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - _ = sender; - _ = e; - Close(); - } - private void OnRootNavigationViewPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { _ = sender; @@ -573,6 +586,10 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext { return; } + + TogglePaneButtonIcon.Icon = RootNavigationView.IsPaneOpen + ? FluentIcons.Common.Icon.LineHorizontal3 + : FluentIcons.Common.Icon.Navigation; } private void UpdateChromeMetrics() @@ -594,8 +611,6 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext RestartNowButton is null || RestartButtonIcon is null || RestartButtonTextBlock is null || - CloseWindowButton is null || - CloseWindowButtonIcon is null || DrawerTitleTextBlock is null || RootNavigationView is null) { @@ -606,7 +621,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext var height = Bounds.Height > 1 ? Bounds.Height : Math.Max(Height, MinHeight); var layoutScale = Math.Clamp(Math.Min(width / 1120d, height / 760d), 0.90, 1.18); - var titleBarHeight = Math.Clamp(48d * layoutScale, 44d, 58d); + const double titleBarHeight = 48d; var titleBarButtonWidth = Math.Clamp(40d * layoutScale, 36d, 48d); var titleBarButtonHeight = Math.Clamp(32d * layoutScale, 30d, 38d); var titleFontSize = Math.Clamp(12d * layoutScale, 11d, 14d); @@ -618,7 +633,6 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext ExtendClientAreaTitleBarHeightHint = titleBarHeight; WindowTitleBarHost.Height = titleBarHeight; - WindowTitleBarHost.Padding = new Thickness(chromePadding, 0, chromePadding, 0); TogglePaneButton.Width = titleBarButtonWidth; TogglePaneButton.Height = titleBarButtonHeight; @@ -636,10 +650,6 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext RestartButtonIcon.FontSize = titleBarIconSize; RestartButtonTextBlock.FontSize = titleFontSize; - CloseWindowButton.Width = titleBarButtonWidth; - CloseWindowButton.Height = titleBarButtonHeight; - CloseWindowButtonIcon.FontSize = titleBarIconSize; - DrawerTitleTextBlock.FontSize = drawerTitleFontSize; } diff --git a/_b.txt b/_b.txt new file mode 100644 index 0000000..9ecc1c2 --- /dev/null +++ b/_b.txt @@ -0,0 +1,13 @@ + 正在确定要还原的项目… + 已还原 D:\github\LanMountainDesktop\LanMountainDesktop.PluginIsolation.Contracts\LanMountainDesktop.PluginIsolation.Contracts.csproj (用时 265 毫秒)。 + 已还原 D:\github\LanMountainDesktop\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj (用时 597 毫秒)。 + 已还原 D:\github\LanMountainDesktop\LanMountainDesktop.Shared.IPC\LanMountainDesktop.Shared.IPC.csproj (用时 264 毫秒)。 +C:\Program Files\dotnet\sdk\10.0.201\NuGet.targets(196,5): error : 磁盘空间不足。 [D:\github\LanMountainDesktop\LanMountainDesktop\LanMountainDesktop.csproj] + +生成失败。 + +C:\Program Files\dotnet\sdk\10.0.201\NuGet.targets(196,5): error : 磁盘空间不足。 [D:\github\LanMountainDesktop\LanMountainDesktop\LanMountainDesktop.csproj] + 0 个警告 + 1 个错误 + +已用时间 00:00:07.94