diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..327b321 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,8 @@ + + + 1.0.0 + net10.0 + enable + enable + + diff --git a/LanMountainDesktop.Appearance/AppearanceCornerRadiusTokenFactory.cs b/LanMountainDesktop.Appearance/AppearanceCornerRadiusTokenFactory.cs new file mode 100644 index 0000000..a0a758b --- /dev/null +++ b/LanMountainDesktop.Appearance/AppearanceCornerRadiusTokenFactory.cs @@ -0,0 +1,27 @@ +using Avalonia; +using LanMountainDesktop.Settings.Core; +using LanMountainDesktop.Shared.Contracts; + +namespace LanMountainDesktop.Appearance; + +public static class AppearanceCornerRadiusTokenFactory +{ + public static AppearanceCornerRadiusTokens Create(double scale) + { + var normalizedScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(scale); + return new AppearanceCornerRadiusTokens( + Radius(6, normalizedScale), + Radius(10, normalizedScale), + Radius(14, normalizedScale), + Radius(18, normalizedScale), + Radius(24, normalizedScale), + Radius(30, normalizedScale), + Radius(36, normalizedScale)); + } + + private static CornerRadius Radius(double value, double scale) + { + var scaled = Math.Round(value * scale * 2, MidpointRounding.AwayFromZero) / 2d; + return new CornerRadius(scaled); + } +} diff --git a/LanMountainDesktop.Appearance/LanMountainDesktop.Appearance.csproj b/LanMountainDesktop.Appearance/LanMountainDesktop.Appearance.csproj new file mode 100644 index 0000000..e202a92 --- /dev/null +++ b/LanMountainDesktop.Appearance/LanMountainDesktop.Appearance.csproj @@ -0,0 +1,13 @@ + + + net10.0 + enable + enable + + + + + + + + diff --git a/LanMountainDesktop.DesktopComponents.Runtime/LanMountainDesktop.DesktopComponents.Runtime.csproj b/LanMountainDesktop.DesktopComponents.Runtime/LanMountainDesktop.DesktopComponents.Runtime.csproj new file mode 100644 index 0000000..14d25b6 --- /dev/null +++ b/LanMountainDesktop.DesktopComponents.Runtime/LanMountainDesktop.DesktopComponents.Runtime.csproj @@ -0,0 +1,14 @@ + + + net10.0 + enable + enable + + + + + + + + + diff --git a/LanMountainDesktop.DesktopHost/DesktopBootstrap.cs b/LanMountainDesktop.DesktopHost/DesktopBootstrap.cs new file mode 100644 index 0000000..6d2e2c3 --- /dev/null +++ b/LanMountainDesktop.DesktopHost/DesktopBootstrap.cs @@ -0,0 +1,27 @@ +using System; +using Avalonia; + +namespace LanMountainDesktop.DesktopHost; + +public static class DesktopBootstrap +{ + public static void InitializeStartupServices(Action initializeDeviceId, Action initializeCrashReporting, Action initializeUserBehaviorAnalytics, Action scheduleStartupCleanup) + { + ArgumentNullException.ThrowIfNull(initializeDeviceId); + ArgumentNullException.ThrowIfNull(initializeCrashReporting); + ArgumentNullException.ThrowIfNull(initializeUserBehaviorAnalytics); + ArgumentNullException.ThrowIfNull(scheduleStartupCleanup); + + initializeDeviceId(); + initializeCrashReporting(); + initializeUserBehaviorAnalytics(); + scheduleStartupCleanup(); + } + + public static void InitializeApplication(Application application, Action initializeShell) + { + ArgumentNullException.ThrowIfNull(application); + ArgumentNullException.ThrowIfNull(initializeShell); + initializeShell(); + } +} diff --git a/LanMountainDesktop.DesktopHost/DesktopShellHost.cs b/LanMountainDesktop.DesktopHost/DesktopShellHost.cs new file mode 100644 index 0000000..2dc756d --- /dev/null +++ b/LanMountainDesktop.DesktopHost/DesktopShellHost.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using LanMountainDesktop.Host.Abstractions; + +namespace LanMountainDesktop.DesktopHost; + +public sealed class DesktopShellHost : IDesktopShellHost +{ + private readonly Action _initializePluginRuntime; + private readonly Action _initializeTrayIcon; + private readonly Action _createAndAssignMainWindow; + private readonly Action _performExitCleanup; + private readonly Action _startActivationListener; + private readonly Action _startWeatherRefresh; + + public DesktopShellHost( + Action initializePluginRuntime, + Action initializeTrayIcon, + Action createAndAssignMainWindow, + Action performExitCleanup, + Action startActivationListener, + Action startWeatherRefresh) + { + _initializePluginRuntime = initializePluginRuntime; + _initializeTrayIcon = initializeTrayIcon; + _createAndAssignMainWindow = createAndAssignMainWindow; + _performExitCleanup = performExitCleanup; + _startActivationListener = startActivationListener; + _startWeatherRefresh = startWeatherRefresh; + } + + public void Initialize() + { + throw new InvalidOperationException("An application instance is required to initialize the desktop shell."); + } + + public void Initialize(Application application) + { + ArgumentNullException.ThrowIfNull(application); + + _initializePluginRuntime(); + _initializeTrayIcon(); + + if (application.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.Exit += (_, _) => _performExitCleanup(); + _createAndAssignMainWindow(desktop); + _startActivationListener(); + } + + _startWeatherRefresh(); + } +} diff --git a/LanMountainDesktop.DesktopHost/DesktopStartupCoordinator.cs b/LanMountainDesktop.DesktopHost/DesktopStartupCoordinator.cs new file mode 100644 index 0000000..efc42dc --- /dev/null +++ b/LanMountainDesktop.DesktopHost/DesktopStartupCoordinator.cs @@ -0,0 +1,15 @@ +using System; + +namespace LanMountainDesktop.DesktopHost; + +public sealed class DesktopStartupCoordinator +{ + private readonly Action _restoreWorkspaceState; + + public DesktopStartupCoordinator(Action restoreWorkspaceState) + { + _restoreWorkspaceState = restoreWorkspaceState ?? throw new ArgumentNullException(nameof(restoreWorkspaceState)); + } + + public void Restore() => _restoreWorkspaceState(); +} diff --git a/LanMountainDesktop.DesktopHost/LanMountainDesktop.DesktopHost.csproj b/LanMountainDesktop.DesktopHost/LanMountainDesktop.DesktopHost.csproj new file mode 100644 index 0000000..acb8b49 --- /dev/null +++ b/LanMountainDesktop.DesktopHost/LanMountainDesktop.DesktopHost.csproj @@ -0,0 +1,18 @@ + + + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/LanMountainDesktop.DesktopHost/SettingsWindowHost.cs b/LanMountainDesktop.DesktopHost/SettingsWindowHost.cs new file mode 100644 index 0000000..e4c2200 --- /dev/null +++ b/LanMountainDesktop.DesktopHost/SettingsWindowHost.cs @@ -0,0 +1,18 @@ +using System; + +namespace LanMountainDesktop.DesktopHost; + +public sealed class SettingsWindowHost +{ + private readonly Action _openSettingsWindow; + + public SettingsWindowHost(Action openSettingsWindow) + { + _openSettingsWindow = openSettingsWindow ?? throw new ArgumentNullException(nameof(openSettingsWindow)); + } + + public void Open(string source, string? pageId = null) + { + _openSettingsWindow(source, pageId); + } +} diff --git a/LanMountainDesktop.DesktopHost/ShutdownCoordinator.cs b/LanMountainDesktop.DesktopHost/ShutdownCoordinator.cs new file mode 100644 index 0000000..a7826ec --- /dev/null +++ b/LanMountainDesktop.DesktopHost/ShutdownCoordinator.cs @@ -0,0 +1,19 @@ +using System; + +namespace LanMountainDesktop.DesktopHost; + +public sealed class ShutdownCoordinator +{ + private readonly Action _prepareForShutdown; + private readonly Action _resetShutdownIntent; + + public ShutdownCoordinator(Action prepareForShutdown, Action resetShutdownIntent) + { + _prepareForShutdown = prepareForShutdown ?? throw new ArgumentNullException(nameof(prepareForShutdown)); + _resetShutdownIntent = resetShutdownIntent ?? throw new ArgumentNullException(nameof(resetShutdownIntent)); + } + + public void Prepare(bool isRestart, string source) => _prepareForShutdown(isRestart, source); + + public void Reset(string source) => _resetShutdownIntent(source); +} diff --git a/LanMountainDesktop.Host.Abstractions/ComponentChromeContext.cs b/LanMountainDesktop.Host.Abstractions/ComponentChromeContext.cs new file mode 100644 index 0000000..110a0bd --- /dev/null +++ b/LanMountainDesktop.Host.Abstractions/ComponentChromeContext.cs @@ -0,0 +1,12 @@ +using LanMountainDesktop.PluginSdk; +using LanMountainDesktop.Shared.Contracts; + +namespace LanMountainDesktop.Host.Abstractions; + +public sealed record ComponentChromeContext( + string ComponentId, + string? PlacementId, + double CellSize, + double GlobalCornerRadiusScale, + AppearanceCornerRadiusTokens CornerRadiusTokens, + SettingsScope Scope = SettingsScope.App); diff --git a/LanMountainDesktop.Host.Abstractions/IDesktopShellHost.cs b/LanMountainDesktop.Host.Abstractions/IDesktopShellHost.cs new file mode 100644 index 0000000..cf5743a --- /dev/null +++ b/LanMountainDesktop.Host.Abstractions/IDesktopShellHost.cs @@ -0,0 +1,6 @@ +namespace LanMountainDesktop.Host.Abstractions; + +public interface IDesktopShellHost +{ + void Initialize(); +} diff --git a/LanMountainDesktop.Host.Abstractions/LanMountainDesktop.Host.Abstractions.csproj b/LanMountainDesktop.Host.Abstractions/LanMountainDesktop.Host.Abstractions.csproj new file mode 100644 index 0000000..d88590a --- /dev/null +++ b/LanMountainDesktop.Host.Abstractions/LanMountainDesktop.Host.Abstractions.csproj @@ -0,0 +1,10 @@ + + + net10.0 + enable + enable + + + + + diff --git a/LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj b/LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj index c7dcfce..d3dc722 100644 --- a/LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj +++ b/LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj @@ -12,6 +12,7 @@ + diff --git a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs index 4181e66..26010eb 100644 --- a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs +++ b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs @@ -1,3 +1,5 @@ +using LanMountainDesktop.Shared.Contracts; + namespace LanMountainDesktop.PluginSdk; public sealed class PluginDesktopComponentContext @@ -11,6 +13,8 @@ public sealed class PluginDesktopComponentContext string componentId, string? placementId, double cellSize, + double globalCornerRadiusScale, + AppearanceCornerRadiusTokens cornerRadiusTokens, IPluginSettingsService? pluginSettings = null) { ArgumentNullException.ThrowIfNull(manifest); @@ -19,6 +23,7 @@ public sealed class PluginDesktopComponentContext ArgumentException.ThrowIfNullOrWhiteSpace(componentId); ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(properties); + ArgumentNullException.ThrowIfNull(cornerRadiusTokens); Manifest = manifest; PluginDirectory = pluginDirectory; @@ -28,6 +33,8 @@ public sealed class PluginDesktopComponentContext ComponentId = componentId.Trim(); PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim(); CellSize = Math.Max(1, cellSize); + GlobalCornerRadiusScale = Math.Max(0.1d, globalCornerRadiusScale); + CornerRadiusTokens = cornerRadiusTokens; PluginSettings = pluginSettings; } @@ -47,8 +54,22 @@ public sealed class PluginDesktopComponentContext public double CellSize { get; } + public double GlobalCornerRadiusScale { get; } + + public AppearanceCornerRadiusTokens CornerRadiusTokens { get; } + public IPluginSettingsService? PluginSettings { get; } + public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null) + { + var scaled = Math.Max(0d, baseRadius) * GlobalCornerRadiusScale; + var scaledMin = minimum.HasValue ? minimum.Value * GlobalCornerRadiusScale : scaled; + var scaledMax = maximum.HasValue ? maximum.Value * GlobalCornerRadiusScale : scaled; + return minimum.HasValue || maximum.HasValue + ? Math.Clamp(scaled, scaledMin, scaledMax) + : scaled; + } + public T? GetService() { return (T?)Services.GetService(typeof(T)); diff --git a/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs b/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs new file mode 100644 index 0000000..185312d --- /dev/null +++ b/LanMountainDesktop.Settings.Core/GlobalAppearanceSettings.cs @@ -0,0 +1,20 @@ +namespace LanMountainDesktop.Settings.Core; + +public static class GlobalAppearanceSettings +{ + public const double DefaultCornerRadiusScale = 1.0; + public const double MinimumCornerRadiusScale = 0.70; + public const double MaximumCornerRadiusScale = 1.40; + public const double CornerRadiusScaleStep = 0.05; + + public static double NormalizeCornerRadiusScale(double value) + { + if (double.IsNaN(value) || double.IsInfinity(value)) + { + return DefaultCornerRadiusScale; + } + + var clamped = Math.Clamp(value, MinimumCornerRadiusScale, MaximumCornerRadiusScale); + return Math.Round(clamped / CornerRadiusScaleStep, MidpointRounding.AwayFromZero) * CornerRadiusScaleStep; + } +} diff --git a/LanMountainDesktop.Settings.Core/LanMountainDesktop.Settings.Core.csproj b/LanMountainDesktop.Settings.Core/LanMountainDesktop.Settings.Core.csproj new file mode 100644 index 0000000..05a9866 --- /dev/null +++ b/LanMountainDesktop.Settings.Core/LanMountainDesktop.Settings.Core.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/LanMountainDesktop.Shared.Contracts/AppearanceCornerRadiusTokens.cs b/LanMountainDesktop.Shared.Contracts/AppearanceCornerRadiusTokens.cs new file mode 100644 index 0000000..6f33088 --- /dev/null +++ b/LanMountainDesktop.Shared.Contracts/AppearanceCornerRadiusTokens.cs @@ -0,0 +1,12 @@ +using Avalonia; + +namespace LanMountainDesktop.Shared.Contracts; + +public sealed record AppearanceCornerRadiusTokens( + CornerRadius Micro, + CornerRadius Xs, + CornerRadius Sm, + CornerRadius Md, + CornerRadius Lg, + CornerRadius Xl, + CornerRadius Island); diff --git a/LanMountainDesktop.Shared.Contracts/LanMountainDesktop.Shared.Contracts.csproj b/LanMountainDesktop.Shared.Contracts/LanMountainDesktop.Shared.Contracts.csproj new file mode 100644 index 0000000..d23bea9 --- /dev/null +++ b/LanMountainDesktop.Shared.Contracts/LanMountainDesktop.Shared.Contracts.csproj @@ -0,0 +1,10 @@ + + + net10.0 + enable + enable + + + + + diff --git a/LanMountainDesktop.slnx b/LanMountainDesktop.slnx index 20053cf..77087f6 100644 --- a/LanMountainDesktop.slnx +++ b/LanMountainDesktop.slnx @@ -1,5 +1,11 @@ + + + + + + diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 4f3e62d..0a8076f 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -15,6 +15,7 @@ using Avalonia.Styling; using Avalonia.Threading; using AvaloniaWebView; using LanMountainDesktop.ComponentSystem; +using LanMountainDesktop.DesktopHost; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; @@ -61,6 +62,7 @@ public partial class App : Application private MainWindow? _mainWindow; private bool _mainWindowClosed; private bool _uiUnhandledExceptionHooked; + private DesktopShellHost? _desktopShellHost; internal static SingleInstanceService? CurrentSingleInstanceService { get; set; } internal static (UserBehaviorAnalyticsService?, CrashReportService?) AnalyticsServices { get; set; } @@ -116,28 +118,32 @@ public partial class App : Application AppLogger.Info("App", "Framework initialization completed."); RegisterUiUnhandledExceptionGuard(); LinuxDesktopEntryInstaller.EnsureInstalled(); - InitializePluginRuntime(); - InitializeTrayIcon(); + DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell); - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - // Avoid duplicate validations from both Avalonia and the CommunityToolkit. - // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins - DisableAvaloniaDataAnnotationValidation(); - desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown; - desktop.Exit += (_, _) => + base.OnFrameworkInitializationCompleted(); + } + + private void InitializeDesktopShell() + { + _desktopShellHost ??= new DesktopShellHost( + InitializePluginRuntime, + InitializeTrayIcon, + desktop => + { + // Avoid duplicate validations from both Avalonia and the CommunityToolkit. + // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins + DisableAvaloniaDataAnnotationValidation(); + desktop.ShutdownMode = Avalonia.Controls.ShutdownMode.OnExplicitShutdown; + CreateAndAssignMainWindow(desktop, "FrameworkInitialization"); + }, + () => { AppLogger.Info("App", "Desktop lifetime exit triggered."); PerformExitCleanup(); - }; - - CreateAndAssignMainWindow(desktop, "FrameworkInitialization"); - CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow); - } - - StartWeatherLocationRefreshIfNeeded(); - - base.OnFrameworkInitializationCompleted(); + }, + () => CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow), + StartWeatherLocationRefreshIfNeeded); + _desktopShellHost.Initialize(this); } private void OnTrayExitClick(object? sender, EventArgs e) @@ -493,6 +499,7 @@ public partial class App : Application refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) || changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) || + changedKeys.Contains(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale), StringComparer.OrdinalIgnoreCase) || (string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) && changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) || (string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeWallpaperMonet, StringComparison.OrdinalIgnoreCase) && diff --git a/LanMountainDesktop/ComponentSystem/DesktopComponentRuntimeContext.cs b/LanMountainDesktop/ComponentSystem/DesktopComponentRuntimeContext.cs index a05fabe..168bde4 100644 --- a/LanMountainDesktop/ComponentSystem/DesktopComponentRuntimeContext.cs +++ b/LanMountainDesktop/ComponentSystem/DesktopComponentRuntimeContext.cs @@ -1,3 +1,4 @@ +using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; @@ -10,5 +11,6 @@ public sealed record DesktopComponentRuntimeContext( ISettingsFacadeService SettingsFacade, ISettingsService SettingsService, IAppearanceThemeService AppearanceTheme, + ComponentChromeContext Chrome, IComponentSettingsAccessor ComponentSettingsAccessor, IComponentInstanceSettingsStore ComponentSettingsStore); diff --git a/LanMountainDesktop/ComponentSystem/IComponentChromeContextAware.cs b/LanMountainDesktop/ComponentSystem/IComponentChromeContextAware.cs new file mode 100644 index 0000000..4f3ef7c --- /dev/null +++ b/LanMountainDesktop/ComponentSystem/IComponentChromeContextAware.cs @@ -0,0 +1,8 @@ +using LanMountainDesktop.Host.Abstractions; + +namespace LanMountainDesktop.ComponentSystem; + +public interface IComponentChromeContextAware +{ + void SetComponentChromeContext(ComponentChromeContext context); +} diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj index 06e054d..f677fb0 100644 --- a/LanMountainDesktop/LanMountainDesktop.csproj +++ b/LanMountainDesktop/LanMountainDesktop.csproj @@ -29,6 +29,12 @@ + + + + + + diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json index e87ef9c..556be17 100644 --- a/LanMountainDesktop/Localization/en-US.json +++ b/LanMountainDesktop/Localization/en-US.json @@ -25,7 +25,7 @@ "settings.nav.group_system": "System", "settings.nav.group_extensions": "Extensions", "settings.nav.wallpaper": "Wallpaper", - "settings.nav.grid": "Grid", + "settings.nav.grid": "Components", "settings.nav.color": "Color", "settings.nav.status_bar": "Status Bar", "settings.nav.weather": "Weather", @@ -303,8 +303,17 @@ "settings.status_bar.clock_format.hm": "Hour:Minute", "settings.status_bar.clock_format.hms": "Hour:Minute:Second", "settings.components.title": "Components", - "settings.components.description": "Adjust desktop grid density and widget placement.", - "settings.components.grid_header": "Grid Layout", + "settings.components.description": "Adjust component layout and corner design.", + "settings.components.grid_header": "Grid Settings", + "settings.components.header": "Grid Settings", + "settings.components.short_side_label": "Short Side Cells", + "settings.components.edge_inset_label": "Screen Inset", + "settings.components.spacing_label": "Component Spacing", + "settings.components.spacing_compact": "Compact", + "settings.components.spacing_relaxed": "Relaxed", + "settings.components.corner_radius.header": "Corner Design", + "settings.components.corner_radius.label": "Component Corner Radius", + "settings.components.corner_radius.description": "Adjust the shared corner radius used by component containers, and expand the internal safe area with it.", "settings.update.title": "Update", "settings.update.current_version_label": "Current Version", "settings.update.latest_version_label": "Latest Release", diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json index 82bacad..bf5fdf0 100644 --- a/LanMountainDesktop/Localization/zh-CN.json +++ b/LanMountainDesktop/Localization/zh-CN.json @@ -25,7 +25,7 @@ "settings.nav.group_system": "系统", "settings.nav.group_extensions": "扩展", "settings.nav.wallpaper": "壁纸", - "settings.nav.grid": "网格", + "settings.nav.grid": "组件", "settings.nav.color": "颜色", "settings.nav.status_bar": "状态栏", "settings.nav.weather": "天气", @@ -301,9 +301,18 @@ "settings.status_bar.clock_format_label": "时钟格式", "settings.status_bar.clock_format.hm": "时:分", "settings.status_bar.clock_format.hms": "时:分:秒", - "settings.components.title": "网格", - "settings.components.description": "调整桌面网格与布局。", - "settings.components.grid_header": "网格布局", + "settings.components.title": "组件", + "settings.components.description": "调整组件布局与圆角设计。", + "settings.components.grid_header": "网格设置", + "settings.components.header": "网格设置", + "settings.components.short_side_label": "短边格数", + "settings.components.edge_inset_label": "屏幕边距", + "settings.components.spacing_label": "组件间距", + "settings.components.spacing_compact": "紧凑", + "settings.components.spacing_relaxed": "宽松", + "settings.components.corner_radius.header": "圆角设计", + "settings.components.corner_radius.label": "组件圆角", + "settings.components.corner_radius.description": "统一调整组件容器圆角,并随圆角增大同步扩展内部安全区。", "settings.update.title": "更新", "settings.update.current_version_label": "当前版本", "settings.update.latest_version_label": "最新发布", diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs index f28c8f9..e691e59 100644 --- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using LanMountainDesktop.Settings.Core; namespace LanMountainDesktop.Models; @@ -16,6 +17,8 @@ public sealed class AppSettingsSnapshot public bool UseSystemChrome { get; set; } + public double GlobalCornerRadiusScale { get; set; } = GlobalAppearanceSettings.DefaultCornerRadiusScale; + public string ThemeColorMode { get; set; } = "default_neutral"; public string SystemMaterialMode { get; set; } = "none"; diff --git a/LanMountainDesktop/Program.cs b/LanMountainDesktop/Program.cs index a8eb726..a6d31d9 100644 --- a/LanMountainDesktop/Program.cs +++ b/LanMountainDesktop/Program.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Avalonia; using Avalonia.WebView.Desktop; +using LanMountainDesktop.DesktopHost; using LanMountainDesktop.Models; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; @@ -20,9 +21,11 @@ sealed class Program { AppLogger.Initialize(); RegisterGlobalExceptionLogging(); - InitializeDeviceId(); - InitializeCrashReporting(); - InitializeUserBehaviorAnalytics(); + DesktopBootstrap.InitializeStartupServices( + InitializeDeviceId, + InitializeCrashReporting, + InitializeUserBehaviorAnalytics, + ScheduleWhiteboardNoteStartupCleanup); var restartParentProcessId = AppRestartService.TryGetRestartParentProcessId(args); using var singleInstance = AcquireSingleInstance(restartParentProcessId); @@ -43,7 +46,6 @@ sealed class Program var diagnostics = StartupDiagnosticsService.Run(args); StartupDiagnosticsService.ShowLegacyExecutableWarningIfNeeded(diagnostics); - ScheduleWhiteboardNoteStartupCleanup(); try { diff --git a/LanMountainDesktop/Services/AppearanceThemeService.cs b/LanMountainDesktop/Services/AppearanceThemeService.cs index 7c6b9e2..295b849 100644 --- a/LanMountainDesktop/Services/AppearanceThemeService.cs +++ b/LanMountainDesktop/Services/AppearanceThemeService.cs @@ -11,9 +11,12 @@ using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; using Avalonia.Media.Imaging; +using LanMountainDesktop.Appearance; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services.Settings; +using LanMountainDesktop.Settings.Core; +using LanMountainDesktop.Shared.Contracts; using LanMountainDesktop.Theme; using Microsoft.Win32; @@ -41,6 +44,8 @@ public sealed record AppearanceThemeSnapshot( string ThemeColorMode, string? UserThemeColor, string? SelectedWallpaperSeed, + double GlobalCornerRadiusScale, + AppearanceCornerRadiusTokens CornerRadiusTokens, string ResolvedSeedSource, MonetPalette MonetPalette, Color AccentColor, @@ -464,6 +469,13 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa var context = CreateThemeContext(snapshot); ThemeColorSystemService.ApplyThemeResources(resources, context); GlassEffectService.ApplyGlassResources(resources, context); + resources["DesignCornerRadiusMicro"] = snapshot.CornerRadiusTokens.Micro; + resources["DesignCornerRadiusXs"] = snapshot.CornerRadiusTokens.Xs; + resources["DesignCornerRadiusSm"] = snapshot.CornerRadiusTokens.Sm; + resources["DesignCornerRadiusMd"] = snapshot.CornerRadiusTokens.Md; + resources["DesignCornerRadiusLg"] = snapshot.CornerRadiusTokens.Lg; + resources["DesignCornerRadiusXl"] = snapshot.CornerRadiusTokens.Xl; + resources["DesignCornerRadiusIsland"] = snapshot.CornerRadiusTokens.Island; } public AppearanceMaterialSurface GetMaterialSurface(MaterialSurfaceRole role) @@ -538,6 +550,7 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa if (!refreshAll && !changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) && !changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) && + !changedKeys.Contains(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale), StringComparer.OrdinalIgnoreCase) && !(respondsToThemeColor && changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase)) && !(respondsToWallpaper && @@ -559,6 +572,8 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa bool queueWallpaperPaletteBuild) { var availableModes = _windowMaterialService.GetAvailableModes(); + var globalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(themeState.GlobalCornerRadiusScale); + var cornerRadiusTokens = AppearanceCornerRadiusTokenFactory.Create(globalCornerRadiusScale); MonetPalette palette; IReadOnlyList wallpaperSeedCandidates; Color effectiveSeedColor; @@ -598,6 +613,8 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa themeColorMode, themeState.ThemeColor, selectedWallpaperSeed, + globalCornerRadiusScale, + cornerRadiusTokens, resolvedSeedSource, palette, ResolveAccentColor(themeColorMode, themeState.ThemeColor, palette), diff --git a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs index 81b63ac..20198ef 100644 --- a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs +++ b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs @@ -122,6 +122,7 @@ public static class DesktopComponentRegistryFactory var pluginSettings = new PluginScopedSettingsService( contribution.Plugin.Manifest.Id, settingsService); + var appearanceSnapshot = HostAppearanceThemeProvider.GetOrCreate().GetCurrent(); var pluginContext = new PluginDesktopComponentContext( contribution.Plugin.Manifest, contribution.Plugin.Context.PluginDirectory, @@ -131,6 +132,8 @@ public static class DesktopComponentRegistryFactory contribution.Registration.ComponentId, context.PlacementId, context.CellSize, + appearanceSnapshot.GlobalCornerRadiusScale, + appearanceSnapshot.CornerRadiusTokens, pluginSettings); return contribution.Registration.ControlFactory(contribution.Plugin.Services, pluginContext); diff --git a/LanMountainDesktop/Services/IComponentLibraryService.cs b/LanMountainDesktop/Services/IComponentLibraryService.cs index fc08ed4..c2fa8c7 100644 --- a/LanMountainDesktop/Services/IComponentLibraryService.cs +++ b/LanMountainDesktop/Services/IComponentLibraryService.cs @@ -20,6 +20,7 @@ public sealed record ComponentLibraryCategoryEntry( public sealed record ComponentLibraryCreateContext( double CellSize, + double GlobalCornerRadiusScale, TimeZoneService TimeZoneService, IWeatherInfoService WeatherInfoService, IRecommendationInfoService RecommendationInfoService, diff --git a/LanMountainDesktop/Services/Settings/SettingsCatalogService.cs b/LanMountainDesktop/Services/Settings/SettingsCatalogService.cs index acd27ff..2e2dada 100644 --- a/LanMountainDesktop/Services/Settings/SettingsCatalogService.cs +++ b/LanMountainDesktop/Services/Settings/SettingsCatalogService.cs @@ -17,7 +17,7 @@ internal sealed class SettingsCatalogService : ISettingsCatalog [ new SettingsSectionDefinition("general", SettingsCategories.General, SettingsScope.App, "settings.general.title", iconKey: "Settings", sortOrder: 0), new SettingsSectionDefinition("appearance", SettingsCategories.Appearance, SettingsScope.App, "settings.appearance.title", iconKey: "DesignIdeas", sortOrder: 10), - new SettingsSectionDefinition("components", SettingsCategories.Components, SettingsScope.ComponentInstance, "settings.components.title", iconKey: "GridDots", sortOrder: 20), + new SettingsSectionDefinition("components", SettingsCategories.Components, SettingsScope.ComponentInstance, "settings.components.title", iconKey: "Apps", sortOrder: 20), new SettingsSectionDefinition("plugins", SettingsCategories.Plugins, SettingsScope.Plugin, "settings.plugins.title", iconKey: "PuzzlePiece", sortOrder: 30), new SettingsSectionDefinition("about", SettingsCategories.About, SettingsScope.App, "settings.about.title", iconKey: "Info", sortOrder: 40) ]); diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs index 7a82e8f..53d30d9 100644 --- a/LanMountainDesktop/Services/Settings/SettingsContracts.cs +++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; +using LanMountainDesktop.Settings.Core; namespace LanMountainDesktop.Services.Settings; @@ -20,6 +21,7 @@ public sealed record ThemeAppearanceSettingsState( bool IsNightMode, string? ThemeColor, bool UseSystemChrome, + double GlobalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale, string ThemeColorMode = ThemeAppearanceValues.ColorModeDefaultNeutral, string SystemMaterialMode = ThemeAppearanceValues.MaterialNone, string? SelectedWallpaperSeed = null); diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs index 6ef48d2..0105f7c 100644 --- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs +++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs @@ -10,6 +10,7 @@ using Avalonia.Media.Imaging; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; +using LanMountainDesktop.Settings.Core; using LanMountainDesktop.Services.PluginMarket; namespace LanMountainDesktop.Services.Settings; @@ -242,6 +243,7 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService snapshot.IsNightMode ?? false, snapshot.ThemeColor, snapshot.UseSystemChrome, + GlobalAppearanceSettings.NormalizeCornerRadiusScale(snapshot.GlobalCornerRadiusScale), ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor), ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode), snapshot.SelectedWallpaperSeed); @@ -252,6 +254,7 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService var snapshot = _settingsService.Load(); var changedKeys = new List(); var normalizedThemeColor = string.IsNullOrWhiteSpace(state.ThemeColor) ? null : state.ThemeColor; + var normalizedCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(state.GlobalCornerRadiusScale); var normalizedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(state.ThemeColorMode, state.ThemeColor); var normalizedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(state.SystemMaterialMode); var normalizedSelectedWallpaperSeed = string.IsNullOrWhiteSpace(state.SelectedWallpaperSeed) @@ -276,6 +279,12 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService changedKeys.Add(nameof(AppSettingsSnapshot.UseSystemChrome)); } + if (Math.Abs(GlobalAppearanceSettings.NormalizeCornerRadiusScale(snapshot.GlobalCornerRadiusScale) - normalizedCornerRadiusScale) > 0.0001d) + { + snapshot.GlobalCornerRadiusScale = normalizedCornerRadiusScale; + changedKeys.Add(nameof(AppSettingsSnapshot.GlobalCornerRadiusScale)); + } + if (!string.Equals(snapshot.ThemeColorMode, normalizedThemeColorMode, StringComparison.OrdinalIgnoreCase)) { snapshot.ThemeColorMode = normalizedThemeColorMode; diff --git a/LanMountainDesktop/Styles/ComponentEditorThemeResources.axaml b/LanMountainDesktop/Styles/ComponentEditorThemeResources.axaml index 43e9588..692f90f 100644 --- a/LanMountainDesktop/Styles/ComponentEditorThemeResources.axaml +++ b/LanMountainDesktop/Styles/ComponentEditorThemeResources.axaml @@ -18,7 +18,7 @@ - + @@ -40,7 +40,7 @@ - + @@ -61,7 +61,7 @@ - + @@ -100,7 +100,7 @@ - + @@ -108,7 +108,7 @@ - + @@ -139,14 +139,14 @@ - + @@ -175,7 +177,7 @@ - + @@ -184,7 +186,7 @@ - + @@ -193,7 +195,7 @@ - + @@ -206,7 +208,7 @@ diff --git a/LanMountainDesktop/Styles/SettingsCardStyles.axaml b/LanMountainDesktop/Styles/SettingsCardStyles.axaml index c9786c4..6ea1c28 100644 --- a/LanMountainDesktop/Styles/SettingsCardStyles.axaml +++ b/LanMountainDesktop/Styles/SettingsCardStyles.axaml @@ -48,21 +48,21 @@ @@ -201,7 +201,7 @@ - + @@ -229,7 +229,7 @@ - + @@ -254,7 +254,7 @@ - + diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs index ba6063a..3b3f397 100644 --- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs +++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs @@ -12,6 +12,7 @@ using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; +using LanMountainDesktop.Settings.Core; namespace LanMountainDesktop.ViewModels; @@ -481,6 +482,9 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase [ObservableProperty] private bool _useSystemChrome; + [ObservableProperty] + private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale; + [ObservableProperty] private SelectionOption _selectedThemeColorMode = new(ThemeAppearanceValues.ColorModeSeedMonet, "User theme color Monet"); @@ -547,6 +551,12 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase [ObservableProperty] private string _systemMaterialLabel = string.Empty; + [ObservableProperty] + private string _globalCornerRadiusLabel = string.Empty; + + [ObservableProperty] + private string _globalCornerRadiusDescription = string.Empty; + [ObservableProperty] private string _themeHeader = string.Empty; @@ -668,6 +678,32 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase PersistCurrentState(restartRequired: false); } + partial void OnGlobalCornerRadiusScaleChanged(double value) + { + if (_isInitializing) + { + return; + } + + var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value); + if (Math.Abs(normalized - value) > 0.0001d) + { + _isInitializing = true; + try + { + GlobalCornerRadiusScale = normalized; + } + finally + { + _isInitializing = false; + } + + return; + } + + PersistCurrentState(restartRequired: false); + } + partial void OnSelectedThemeColorModeChanged(SelectionOption value) { if (_isInitializing || value is null) @@ -732,6 +768,8 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color"); ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source"); SystemMaterialLabel = L("settings.appearance.system_material_label", "System material"); + GlobalCornerRadiusLabel = L("settings.appearance.corner_radius.label", "Global corner radius"); + GlobalCornerRadiusDescription = L("settings.appearance.corner_radius.description", "Adjust the shared radius scale used by cards, panels, and component containers."); ThemeSourceNeutralText = L("settings.appearance.theme_color_mode.neutral", "Default neutral"); ThemeSourceUserColorText = L("settings.appearance.theme_color_mode.user", "User theme color Monet"); ThemeSourceWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet"); @@ -776,6 +814,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase IsNightMode = theme.IsNightMode; ThemeColor = theme.ThemeColor ?? string.Empty; UseSystemChrome = theme.UseSystemChrome; + GlobalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(theme.GlobalCornerRadiusScale); _selectedWallpaperSeed = theme.SelectedWallpaperSeed; SyncCustomSeedPickerWithSavedThemeColor(); @@ -825,6 +864,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase IsNightMode, themeColor, UseSystemChrome, + GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale), themeColorMode, ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value), _selectedWallpaperSeed); @@ -956,7 +996,7 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase private string _pageDescription = string.Empty; [ObservableProperty] - private string _gridHeader = string.Empty; + private string _componentsHeader = string.Empty; [ObservableProperty] private string _shortSideCellsLabel = string.Empty; @@ -967,6 +1007,18 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase [ObservableProperty] private string _spacingPresetLabel = string.Empty; + [ObservableProperty] + private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale; + + [ObservableProperty] + private string _componentRadiusHeader = string.Empty; + + [ObservableProperty] + private string _globalCornerRadiusLabel = string.Empty; + + [ObservableProperty] + private string _globalCornerRadiusDescription = string.Empty; + public void Load() { var state = _settingsFacade.Grid.Get(); @@ -976,6 +1028,9 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase SelectedSpacingPreset = SpacingPresets.FirstOrDefault(option => string.Equals(option.Value, spacingPreset, StringComparison.OrdinalIgnoreCase)) ?? SpacingPresets[1]; + + var theme = _settingsFacade.Theme.Get(); + GlobalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(theme.GlobalCornerRadiusScale); } partial void OnShortSideCellsChanged(int value) @@ -1008,6 +1063,32 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase SaveGrid(); } + partial void OnGlobalCornerRadiusScaleChanged(double value) + { + if (_isInitializing) + { + return; + } + + var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value); + if (Math.Abs(normalized - value) > 0.0001d) + { + _isInitializing = true; + try + { + GlobalCornerRadiusScale = normalized; + } + finally + { + _isInitializing = false; + } + + return; + } + + SaveComponentCornerRadius(); + } + private void SaveGrid() { _settingsFacade.Grid.Save(new GridSettingsState( @@ -1016,23 +1097,39 @@ public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase Math.Clamp(EdgeInsetPercent, 0, 30))); } + private void SaveComponentCornerRadius() + { + var theme = _settingsFacade.Theme.Get(); + _settingsFacade.Theme.Save(new ThemeAppearanceSettingsState( + theme.IsNightMode, + theme.ThemeColor, + theme.UseSystemChrome, + GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale), + theme.ThemeColorMode, + theme.SystemMaterialMode, + theme.SelectedWallpaperSeed)); + } + private IReadOnlyList CreateSpacingPresets() { return [ - new SelectionOption("Compact", L("settings.grid.spacing_compact", "Compact")), - new SelectionOption("Relaxed", L("settings.grid.spacing_relaxed", "Relaxed")) + new SelectionOption("Compact", L("settings.components.spacing_compact", "Compact")), + new SelectionOption("Relaxed", L("settings.components.spacing_relaxed", "Relaxed")) ]; } private void RefreshLocalizedText() { PageTitle = L("settings.components.title", "Components"); - PageDescription = L("settings.components.description", "Desktop grid and widget placement density."); - GridHeader = L("settings.components.grid_header", "Grid Layout"); - ShortSideCellsLabel = L("settings.grid.short_side_label", "Short Side Cells"); - EdgeInsetPercentLabel = L("settings.grid.edge_inset_label", "Screen Inset"); - SpacingPresetLabel = L("settings.grid.spacing_label", "Grid Spacing"); + PageDescription = L("settings.components.description", "Adjust component layout and corner design."); + ComponentsHeader = L("settings.components.header", "Grid Settings"); + ShortSideCellsLabel = L("settings.components.short_side_label", "Short Side Cells"); + EdgeInsetPercentLabel = L("settings.components.edge_inset_label", "Screen Inset"); + SpacingPresetLabel = L("settings.components.spacing_label", "Component Spacing"); + ComponentRadiusHeader = L("settings.components.corner_radius.header", "Corner Design"); + GlobalCornerRadiusLabel = L("settings.components.corner_radius.label", "Component Corner Radius"); + GlobalCornerRadiusDescription = L("settings.components.corner_radius.description", "Adjust the shared corner radius used by component containers, and expand the internal safe area with it."); } private string L(string key, string fallback) diff --git a/LanMountainDesktop/Views/ComponentEditorWindow.axaml b/LanMountainDesktop/Views/ComponentEditorWindow.axaml index dc35e67..d38de29 100644 --- a/LanMountainDesktop/Views/ComponentEditorWindow.axaml +++ b/LanMountainDesktop/Views/ComponentEditorWindow.axaml @@ -53,7 +53,7 @@