diff --git a/.arts/settings.json b/.arts/settings.json new file mode 100644 index 0000000..701b3b0 --- /dev/null +++ b/.arts/settings.json @@ -0,0 +1,3 @@ +{ + "diffEditor.renderSideBySide": false +} \ No newline at end of file diff --git a/LanMontainDesktop/App.axaml b/LanMontainDesktop/App.axaml index d416fcd..8bf04af 100644 --- a/LanMontainDesktop/App.axaml +++ b/LanMontainDesktop/App.axaml @@ -1,6 +1,7 @@ @@ -12,5 +13,32 @@ + + + + + + + + + + + diff --git a/LanMontainDesktop/LanMontainDesktop.csproj b/LanMontainDesktop/LanMontainDesktop.csproj index 979e699..fe146e4 100644 --- a/LanMontainDesktop/LanMontainDesktop.csproj +++ b/LanMontainDesktop/LanMontainDesktop.csproj @@ -25,5 +25,8 @@ + + + diff --git a/LanMontainDesktop/Models/AppSettingsSnapshot.cs b/LanMontainDesktop/Models/AppSettingsSnapshot.cs new file mode 100644 index 0000000..10571ec --- /dev/null +++ b/LanMontainDesktop/Models/AppSettingsSnapshot.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace LanMontainDesktop.Models; + +public sealed class AppSettingsSnapshot +{ + public int GridShortSideCells { get; set; } = 12; + + public bool? IsNightMode { get; set; } + + public string? ThemeColor { get; set; } + + public string? WallpaperPath { get; set; } + + public string WallpaperPlacement { get; set; } = "Fill"; + + public int SettingsTabIndex { get; set; } = 0; + + public List TopStatusComponentIds { get; set; } = []; + + public List PinnedTaskbarActions { get; set; } = + [ + TaskbarActionId.MinimizeToWindows.ToString(), + TaskbarActionId.OpenSettings.ToString() + ]; + + public bool EnableDynamicTaskbarActions { get; set; } = false; + + public string TaskbarLayoutMode { get; set; } = "BottomFullRowMacStyle"; +} diff --git a/LanMontainDesktop/Models/TaskbarActionId.cs b/LanMontainDesktop/Models/TaskbarActionId.cs new file mode 100644 index 0000000..98bba5f --- /dev/null +++ b/LanMontainDesktop/Models/TaskbarActionId.cs @@ -0,0 +1,8 @@ +namespace LanMontainDesktop.Models; + +public enum TaskbarActionId +{ + MinimizeToWindows, + OpenSettings +} + diff --git a/LanMontainDesktop/Models/TaskbarActionItem.cs b/LanMontainDesktop/Models/TaskbarActionItem.cs new file mode 100644 index 0000000..8189f22 --- /dev/null +++ b/LanMontainDesktop/Models/TaskbarActionItem.cs @@ -0,0 +1,9 @@ +namespace LanMontainDesktop.Models; + +public sealed record TaskbarActionItem( + TaskbarActionId Id, + string Title, + string IconKey, + bool IsVisible, + string CommandKey); + diff --git a/LanMontainDesktop/Models/TaskbarContext.cs b/LanMontainDesktop/Models/TaskbarContext.cs new file mode 100644 index 0000000..9ac18f9 --- /dev/null +++ b/LanMontainDesktop/Models/TaskbarContext.cs @@ -0,0 +1,10 @@ +namespace LanMontainDesktop.Models; + +public enum TaskbarContext +{ + Desktop, + SettingsWallpaper, + SettingsGrid, + SettingsColor, + SettingsStatusBar +} diff --git a/LanMontainDesktop/Services/AppSettingsService.cs b/LanMontainDesktop/Services/AppSettingsService.cs new file mode 100644 index 0000000..12aeffe --- /dev/null +++ b/LanMontainDesktop/Services/AppSettingsService.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Text.Json; +using LanMontainDesktop.Models; + +namespace LanMontainDesktop.Services; + +public sealed class AppSettingsService +{ + private static readonly JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true + }; + + private readonly string _settingsPath; + + public AppSettingsService() + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var settingsDirectory = Path.Combine(appData, "LanMontainDesktop"); + _settingsPath = Path.Combine(settingsDirectory, "settings.json"); + } + + public AppSettingsSnapshot Load() + { + try + { + if (!File.Exists(_settingsPath)) + { + return new AppSettingsSnapshot(); + } + + var json = File.ReadAllText(_settingsPath); + var snapshot = JsonSerializer.Deserialize(json, SerializerOptions); + return snapshot ?? new AppSettingsSnapshot(); + } + catch + { + return new AppSettingsSnapshot(); + } + } + + public void Save(AppSettingsSnapshot snapshot) + { + try + { + var directory = Path.GetDirectoryName(_settingsPath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + var json = JsonSerializer.Serialize(snapshot, SerializerOptions); + File.WriteAllText(_settingsPath, json); + } + catch + { + // Swallow persistence errors to keep UI interactions uninterrupted. + } + } +} + diff --git a/LanMontainDesktop/Services/GlassEffectService.cs b/LanMontainDesktop/Services/GlassEffectService.cs index 6403893..47b04a5 100644 --- a/LanMontainDesktop/Services/GlassEffectService.cs +++ b/LanMontainDesktop/Services/GlassEffectService.cs @@ -1,32 +1,50 @@ using Avalonia.Controls; using Avalonia.Media; +using LanMontainDesktop.Theme; namespace LanMontainDesktop.Services; public static class GlassEffectService { - public static void ApplyGlassResources(IResourceDictionary resources, bool isLightBackground) - { - if (isLightBackground) - { - resources["AdaptiveButtonBackgroundBrush"] = new SolidColorBrush(Color.Parse("#80FFFFFF")); - resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(Color.Parse("#80475569")); - resources["AdaptiveButtonHoverBackgroundBrush"] = new SolidColorBrush(Color.Parse("#B3FFFFFF")); - resources["AdaptiveButtonPressedBackgroundBrush"] = new SolidColorBrush(Color.Parse("#D9F8FAFC")); - resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush(Color.Parse("#A6FFFFFF")); - resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush(Color.Parse("#80475569")); - resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush(Color.Parse("#CCFFFFFF")); - resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush(Color.Parse("#80475569")); - return; - } + private const double DayPanelBlurRadius = 40; + private const double DayStrongBlurRadius = 60; + private const double DayOverlayBlurRadius = 80; + private const double NightPanelBlurRadius = 45; + private const double NightStrongBlurRadius = 65; + private const double NightOverlayBlurRadius = 85; - resources["AdaptiveButtonBackgroundBrush"] = new SolidColorBrush(Color.Parse("#66334155")); - resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(Color.Parse("#80E2E8F0")); - resources["AdaptiveButtonHoverBackgroundBrush"] = new SolidColorBrush(Color.Parse("#88475A74")); - resources["AdaptiveButtonPressedBackgroundBrush"] = new SolidColorBrush(Color.Parse("#AA2A3B55")); - resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush(Color.Parse("#70233448")); - resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush(Color.Parse("#70475569")); - resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush(Color.Parse("#A01E293B")); - resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush(Color.Parse("#80475569")); + public static void ApplyGlassResources(IResourceDictionary resources, ThemeColorContext context) + { + var neutralBase = context.IsNightMode ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"); + var neutralElevated = context.IsNightMode ? Color.Parse("#FF1E293B") : Color.Parse("#FFFFFFFF"); + var tintMix = context.IsNightMode ? 0.15 : 0.08; + var panelTone = ColorMath.Blend(neutralElevated, context.AccentColor, tintMix); + var strongTone = ColorMath.Blend(neutralBase, context.AccentColor, context.IsNightMode ? 0.18 : 0.12); + var overlayTone = ColorMath.Blend(neutralBase, context.AccentColor, context.IsNightMode ? 0.25 : 0.15); + + resources["AdaptiveButtonBackgroundBrush"] = new SolidColorBrush( + ColorMath.WithAlpha(panelTone, context.IsNightMode ? (byte)0x66 : (byte)0x80)); + resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(ColorMath.WithAlpha(neutralElevated, 0x1A)); + resources["AdaptiveButtonHoverBackgroundBrush"] = new SolidColorBrush( + ColorMath.WithAlpha(ColorMath.Blend(panelTone, context.AccentColor, 0.15), context.IsNightMode ? (byte)0x7A : (byte)0x99)); + resources["AdaptiveButtonPressedBackgroundBrush"] = new SolidColorBrush( + ColorMath.WithAlpha(ColorMath.Blend(panelTone, context.AccentColor, 0.28), context.IsNightMode ? (byte)0x8C : (byte)0xB3)); + + resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush( + ColorMath.WithAlpha(panelTone, context.IsNightMode ? (byte)0x4D : (byte)0x66)); + resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush(ColorMath.WithAlpha(neutralElevated, 0x26)); + resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush( + ColorMath.WithAlpha(strongTone, context.IsNightMode ? (byte)0x66 : (byte)0x80)); + resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush(ColorMath.WithAlpha(neutralElevated, 0x33)); + resources["AdaptiveGlassOverlayBackgroundBrush"] = new SolidColorBrush( + ColorMath.WithAlpha(overlayTone, context.IsNightMode ? (byte)0x59 : (byte)0x73)); + + resources["AdaptiveGlassPanelBlurRadius"] = context.IsNightMode ? NightPanelBlurRadius : DayPanelBlurRadius; + resources["AdaptiveGlassStrongBlurRadius"] = context.IsNightMode ? NightStrongBlurRadius : DayStrongBlurRadius; + resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? NightOverlayBlurRadius : DayOverlayBlurRadius; + resources["AdaptiveGlassPanelOpacity"] = context.IsNightMode ? 0.85 : 0.80; + resources["AdaptiveGlassStrongOpacity"] = context.IsNightMode ? 0.90 : 0.85; + resources["AdaptiveGlassOverlayOpacity"] = context.IsNightMode ? 0.75 : 0.70; + resources["AdaptiveGlassNoiseOpacity"] = context.IsNightMode ? 0.03 : 0.02; } } diff --git a/LanMontainDesktop/Services/ThemeColorSystemService.cs b/LanMontainDesktop/Services/ThemeColorSystemService.cs index cd5c5a0..831672a 100644 --- a/LanMontainDesktop/Services/ThemeColorSystemService.cs +++ b/LanMontainDesktop/Services/ThemeColorSystemService.cs @@ -1,4 +1,3 @@ -using System; using Avalonia.Controls; using Avalonia.Media; using LanMontainDesktop.Theme; @@ -7,21 +6,50 @@ namespace LanMontainDesktop.Services; public static class ThemeColorSystemService { + private const double WcagNormalTextContrast = 4.5; + private const double WcagLargeTextContrast = 3.0; + public static void ApplyThemeResources( IResourceDictionary resources, ThemeColorContext context) { var palette = BuildPalette(context); + resources["AdaptivePrimaryBrush"] = new SolidColorBrush(palette.Primary); + resources["AdaptiveSecondaryBrush"] = new SolidColorBrush(palette.Secondary); + resources["AdaptiveAccentBrush"] = new SolidColorBrush(palette.Accent); + resources["AdaptiveOnAccentBrush"] = new SolidColorBrush(palette.OnAccent); + resources["AdaptiveSurfaceBaseBrush"] = new SolidColorBrush(palette.SurfaceBase); + resources["AdaptiveSurfaceRaisedBrush"] = new SolidColorBrush(palette.SurfaceRaised); + resources["AdaptiveSurfaceOverlayBrush"] = new SolidColorBrush(palette.SurfaceOverlay); resources["AdaptiveTextPrimaryBrush"] = new SolidColorBrush(palette.TextPrimary); resources["AdaptiveTextSecondaryBrush"] = new SolidColorBrush(palette.TextSecondary); resources["AdaptiveTextMutedBrush"] = new SolidColorBrush(palette.TextMuted); resources["AdaptiveTextAccentBrush"] = new SolidColorBrush(palette.TextAccent); resources["AdaptiveNavTextBrush"] = new SolidColorBrush(palette.NavText); resources["AdaptiveNavSelectedTextBrush"] = new SolidColorBrush(palette.NavSelectedText); + resources["AdaptiveNavSelectionIndicatorBrush"] = new SolidColorBrush(palette.NavSelectionIndicator); resources["AdaptiveNavItemBackgroundBrush"] = new SolidColorBrush(palette.NavItemBackground); resources["AdaptiveNavItemHoverBackgroundBrush"] = new SolidColorBrush(palette.NavItemHoverBackground); resources["AdaptiveNavItemSelectedBackgroundBrush"] = new SolidColorBrush(palette.NavItemSelectedBackground); + resources["AdaptiveToggleOnBrush"] = new SolidColorBrush(palette.ToggleOn); + resources["AdaptiveToggleOffBrush"] = new SolidColorBrush(palette.ToggleOff); + resources["AdaptiveToggleBorderBrush"] = new SolidColorBrush(palette.ToggleBorder); + + resources["SystemAccentColor"] = palette.Accent; + resources["SystemAccentColorLight1"] = palette.AccentLight1; + resources["SystemAccentColorLight2"] = palette.AccentLight2; + resources["SystemAccentColorLight3"] = palette.AccentLight3; + resources["SystemAccentColorDark1"] = palette.AccentDark1; + resources["SystemAccentColorDark2"] = palette.AccentDark2; + resources["SystemAccentColorDark3"] = palette.AccentDark3; + resources["SystemAccentColorLight1Brush"] = new SolidColorBrush(palette.AccentLight1); + resources["SystemAccentColorLight2Brush"] = new SolidColorBrush(palette.AccentLight2); + resources["SystemAccentColorLight3Brush"] = new SolidColorBrush(palette.AccentLight3); + resources["SystemAccentColorDark1Brush"] = new SolidColorBrush(palette.AccentDark1); + resources["SystemAccentColorDark2Brush"] = new SolidColorBrush(palette.AccentDark2); + resources["SystemAccentColorDark3Brush"] = new SolidColorBrush(palette.AccentDark3); + resources["SystemAccentColorBrush"] = new SolidColorBrush(palette.Accent); } public static void ApplyThemeResources( @@ -39,42 +67,81 @@ public static class ThemeColorSystemService private static AppThemePalette BuildPalette(ThemeColorContext context) { - var textPrimary = context.IsLightBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"); - var textSecondary = context.IsLightBackground ? Color.Parse("#FF1E293B") : Color.Parse("#FFE2E8F0"); - var textMuted = context.IsLightBackground ? Color.Parse("#FF475569") : Color.Parse("#FF94A3B8"); - var textAccent = context.IsLightBackground - ? BlendColor(context.AccentColor, Color.Parse("#FF0B1220"), 0.20) - : BlendColor(context.AccentColor, Color.Parse("#FFFFFFFF"), 0.16); + var accent = context.AccentColor; + var accentLight1 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.22); + var accentLight2 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.38); + var accentLight3 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.54); + var accentDark1 = ColorMath.Blend(accent, Color.Parse("#FF0B1220"), 0.16); + var accentDark2 = ColorMath.Blend(accent, Color.Parse("#FF0B1220"), 0.28); + var accentDark3 = ColorMath.Blend(accent, Color.Parse("#FF020617"), 0.40); - var navText = context.IsLightNavBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"); - var navSelectedText = Color.Parse("#FFFFFFFF"); - var navItemBackground = context.IsLightNavBackground ? Color.Parse("#40FFFFFF") : Color.Parse("#220F172A"); - var navItemHoverBackground = context.IsLightNavBackground ? Color.Parse("#66E2E8F0") : Color.Parse("#40334155"); - var navItemSelectedBackground = Color.FromArgb( - 0xCC, - context.AccentColor.R, - context.AccentColor.G, - context.AccentColor.B); + var primary = context.IsNightMode ? accentLight1 : accentDark1; + var secondary = context.IsNightMode ? accentLight2 : accentDark2; + + var surfaceBase = context.IsNightMode ? Color.Parse("#FF0B1220") : Color.Parse("#FFF3F7FB"); + var surfaceRaised = context.IsNightMode ? Color.Parse("#FF1E293B") : Color.Parse("#FFFFFFFF"); + var surfaceOverlay = context.IsNightMode ? Color.Parse("#CC0B1220") : Color.Parse("#CCE2E8F0"); + + var textPrimaryPreferred = context.IsLightBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"); + var textPrimary = ColorMath.EnsureContrast(textPrimaryPreferred, surfaceRaised, WcagNormalTextContrast); + var textSecondary = ColorMath.EnsureContrast( + ColorMath.Blend(textPrimary, surfaceRaised, context.IsNightMode ? 0.24 : 0.44), + surfaceRaised, + WcagLargeTextContrast); + var textMuted = ColorMath.EnsureContrast( + ColorMath.Blend(textPrimary, surfaceRaised, context.IsNightMode ? 0.40 : 0.58), + surfaceRaised, + WcagLargeTextContrast); + var textAccent = context.IsLightBackground + ? ColorMath.EnsureContrast(ColorMath.Blend(accent, Color.Parse("#FF0B1220"), 0.20), surfaceRaised, WcagNormalTextContrast) + : ColorMath.EnsureContrast(ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.16), surfaceRaised, WcagNormalTextContrast); + + var navSurface = context.IsLightNavBackground ? surfaceRaised : Color.Parse("#FF111827"); + var navText = ColorMath.EnsureContrast( + context.IsLightNavBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"), + navSurface, + WcagNormalTextContrast); + + var selectedSurfaceForContrast = ColorMath.Blend(accent, navSurface, 0.18); + var navSelectedText = ColorMath.EnsureContrast(Color.Parse("#FFFFFFFF"), selectedSurfaceForContrast, WcagNormalTextContrast); + var navItemBackground = context.IsLightNavBackground ? Color.Parse("#33FFFFFF") : Color.Parse("#2A0F172A"); + var navItemHoverBackground = context.IsLightNavBackground + ? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, Color.Parse("#FFFFFFFF"), 0.48), 0x66) + : ColorMath.WithAlpha(ColorMath.Blend(accentDark1, Color.Parse("#33111827"), 0.32), 0x78); + var navItemSelectedBackground = ColorMath.WithAlpha(accent, context.IsNightMode ? (byte)0xCE : (byte)0xD9); + var navSelectionIndicator = ColorMath.EnsureContrast(accentLight1, navSurface, WcagLargeTextContrast); + + var toggleOn = context.IsNightMode ? accent : accentDark1; + var toggleOff = context.IsNightMode ? Color.Parse("#66475569") : Color.Parse("#66CBD5E1"); + var toggleBorder = context.IsNightMode ? Color.Parse("#80E2E8F0") : Color.Parse("#8094A3B8"); + var onAccent = ColorMath.EnsureContrast(Color.Parse("#FFFFFFFF"), accent, WcagNormalTextContrast); return new AppThemePalette( + primary, + secondary, + accent, + onAccent, + accentLight1, + accentLight2, + accentLight3, + accentDark1, + accentDark2, + accentDark3, + surfaceBase, + surfaceRaised, + surfaceOverlay, textPrimary, textSecondary, textMuted, textAccent, navText, navSelectedText, + navSelectionIndicator, navItemBackground, navItemHoverBackground, - navItemSelectedBackground); - } - - private static Color BlendColor(Color from, Color to, double ratio) - { - ratio = Math.Clamp(ratio, 0, 1); - var inverse = 1 - ratio; - var red = (byte)Math.Round((from.R * inverse) + (to.R * ratio)); - var green = (byte)Math.Round((from.G * inverse) + (to.G * ratio)); - var blue = (byte)Math.Round((from.B * inverse) + (to.B * ratio)); - return Color.FromRgb(red, green, blue); + navItemSelectedBackground, + toggleOn, + toggleOff, + toggleBorder); } } diff --git a/LanMontainDesktop/Styles/GlassModule.axaml b/LanMontainDesktop/Styles/GlassModule.axaml index 494d295..0880e0d 100644 --- a/LanMontainDesktop/Styles/GlassModule.axaml +++ b/LanMontainDesktop/Styles/GlassModule.axaml @@ -2,25 +2,38 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="using:FluentAvalonia.UI.Controls"> + + @@ -30,8 +43,8 @@ @@ -39,16 +52,41 @@ + + + + + + diff --git a/LanMontainDesktop/Theme/AppThemePalette.cs b/LanMontainDesktop/Theme/AppThemePalette.cs index 5a64f28..2cd9a2f 100644 --- a/LanMontainDesktop/Theme/AppThemePalette.cs +++ b/LanMontainDesktop/Theme/AppThemePalette.cs @@ -3,12 +3,29 @@ using Avalonia.Media; namespace LanMontainDesktop.Theme; public sealed record AppThemePalette( + Color Primary, + Color Secondary, + Color Accent, + Color OnAccent, + Color AccentLight1, + Color AccentLight2, + Color AccentLight3, + Color AccentDark1, + Color AccentDark2, + Color AccentDark3, + Color SurfaceBase, + Color SurfaceRaised, + Color SurfaceOverlay, Color TextPrimary, Color TextSecondary, Color TextMuted, Color TextAccent, Color NavText, Color NavSelectedText, + Color NavSelectionIndicator, Color NavItemBackground, Color NavItemHoverBackground, - Color NavItemSelectedBackground); + Color NavItemSelectedBackground, + Color ToggleOn, + Color ToggleOff, + Color ToggleBorder); diff --git a/LanMontainDesktop/Theme/ColorMath.cs b/LanMontainDesktop/Theme/ColorMath.cs new file mode 100644 index 0000000..046d9bb --- /dev/null +++ b/LanMontainDesktop/Theme/ColorMath.cs @@ -0,0 +1,61 @@ +using System; +using Avalonia.Media; + +namespace LanMontainDesktop.Theme; + +public static class ColorMath +{ + public static Color Blend(Color from, Color to, double ratio) + { + ratio = Math.Clamp(ratio, 0, 1); + var inverse = 1 - ratio; + var red = (byte)Math.Round((from.R * inverse) + (to.R * ratio)); + var green = (byte)Math.Round((from.G * inverse) + (to.G * ratio)); + var blue = (byte)Math.Round((from.B * inverse) + (to.B * ratio)); + return Color.FromRgb(red, green, blue); + } + + public static Color WithAlpha(Color color, byte alpha) + { + return Color.FromArgb(alpha, color.R, color.G, color.B); + } + + public static Color EnsureContrast(Color preferred, Color background, double minRatio) + { + if (ContrastRatio(preferred, background) >= minRatio) + { + return preferred; + } + + var white = Color.Parse("#FFFFFFFF"); + var black = Color.Parse("#FF000000"); + var whiteRatio = ContrastRatio(white, background); + var blackRatio = ContrastRatio(black, background); + return whiteRatio >= blackRatio ? white : black; + } + + public static double ContrastRatio(Color first, Color second) + { + var firstLum = RelativeLuminance(first); + var secondLum = RelativeLuminance(second); + var lighter = Math.Max(firstLum, secondLum); + var darker = Math.Min(firstLum, secondLum); + return (lighter + 0.05) / (darker + 0.05); + } + + private static double RelativeLuminance(Color color) + { + var red = ToLinear(color.R / 255d); + var green = ToLinear(color.G / 255d); + var blue = ToLinear(color.B / 255d); + return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue); + } + + private static double ToLinear(double value) + { + return value <= 0.04045 + ? value / 12.92 + : Math.Pow((value + 0.055) / 1.055, 2.4); + } +} + diff --git a/LanMontainDesktop/Views/MainWindow.Settings.cs b/LanMontainDesktop/Views/MainWindow.Settings.cs new file mode 100644 index 0000000..b43b718 --- /dev/null +++ b/LanMontainDesktop/Views/MainWindow.Settings.cs @@ -0,0 +1,1187 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Platform.Storage; +using Avalonia.Styling; +using Avalonia.Threading; +using LanMontainDesktop.Models; +using LanMontainDesktop.Services; +using LanMontainDesktop.Theme; +using LibVLCSharp.Shared; + +namespace LanMontainDesktop.Views; + +public partial class MainWindow +{ + private void OnOpenSettingsClick(object? sender, RoutedEventArgs e) + { + OpenSettingsPage(); + } + + private void OnCloseSettingsClick(object? sender, RoutedEventArgs e) + { + CloseSettingsPage(); + } + + private void OnSettingsNavSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + UpdateSettingsTabContent(); + PersistSettings(); + } + + private void UpdateSettingsTabContent() + { + // SelectionChanged can fire during XAML initialization before all named controls are assigned. + if (SettingsNavListBox is null || + GridSettingsPanel is null || + WallpaperSettingsPanel is null || + ColorSettingsPanel is null || + StatusBarSettingsPanel is null) + { + return; + } + + var selectedIndex = SettingsNavListBox.SelectedIndex; + WallpaperSettingsPanel.IsVisible = selectedIndex == 0; + GridSettingsPanel.IsVisible = selectedIndex == 1; + ColorSettingsPanel.IsVisible = selectedIndex == 2; + StatusBarSettingsPanel.IsVisible = selectedIndex == 3; + ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); + } + + private void OnStatusBarClockChecked(object? sender, RoutedEventArgs e) + { + if (_suppressStatusBarToggleEvents) + { + return; + } + + _topStatusComponentIds.Add(ClockStatusComponentId); + ApplyTopStatusComponentVisibility(); + UpdateWallpaperPreviewLayout(); + PersistSettings(); + } + + private void OnStatusBarClockUnchecked(object? sender, RoutedEventArgs e) + { + if (_suppressStatusBarToggleEvents) + { + return; + } + + _topStatusComponentIds.Remove(ClockStatusComponentId); + ApplyTopStatusComponentVisibility(); + UpdateWallpaperPreviewLayout(); + PersistSettings(); + } + + private void OnNightModeChecked(object? sender, RoutedEventArgs e) + { + if (_suppressThemeToggleEvents) + { + return; + } + + ApplyNightModeState(true, refreshPalettes: true); + } + + private void OnNightModeUnchecked(object? sender, RoutedEventArgs e) + { + if (_suppressThemeToggleEvents) + { + return; + } + + ApplyNightModeState(false, refreshPalettes: true); + } + + private void OnRecommendedColorClick(object? sender, RoutedEventArgs e) + { + ApplyThemeColorFromButton(sender as Button, "Recommended"); + } + + private void OnMonetColorClick(object? sender, RoutedEventArgs e) + { + ApplyThemeColorFromButton(sender as Button, "Monet"); + } + + private void OnRefreshMonetColorsClick(object? sender, RoutedEventArgs e) + { + RefreshColorPalettes(); + EnsureSelectedThemeColor(); + UpdateThemeColorSelectionState(); + ThemeColorStatusTextBlock.Text = "Monet colors refreshed."; + UpdateAdaptiveTextSystem(); + PersistSettings(); + } + + private async void OnPickWallpaperClick(object? sender, RoutedEventArgs e) + { + if (StorageProvider is null) + { + _wallpaperStatus = "Storage provider is unavailable."; + UpdateWallpaperDisplay(); + return; + } + + var options = new FilePickerOpenOptions + { + Title = "Select wallpaper", + AllowMultiple = false, + FileTypeFilter = + [ + new FilePickerFileType("Image files") + { + Patterns = ["*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif", "*.webp"] + }, + new FilePickerFileType("Video files") + { + Patterns = ["*.mp4", "*.mkv", "*.webm", "*.avi", "*.mov", "*.m4v"] + } + ] + }; + + var files = await StorageProvider.OpenFilePickerAsync(options); + if (files.Count == 0) + { + return; + } + + var file = files[0]; + try + { + var importedPath = await ImportWallpaperAssetAsync(file); + if (string.IsNullOrWhiteSpace(importedPath)) + { + _wallpaperStatus = "Failed to import wallpaper file."; + UpdateWallpaperDisplay(); + return; + } + + _wallpaperPath = importedPath; + var mediaType = DetectWallpaperMediaType(importedPath); + switch (mediaType) + { + case WallpaperMediaType.Image: + _wallpaperBitmap?.Dispose(); + _wallpaperBitmap = new Bitmap(importedPath); + _wallpaperVideoPath = null; + _wallpaperMediaType = WallpaperMediaType.Image; + _wallpaperStatus = "Image wallpaper applied."; + break; + case WallpaperMediaType.Video: + _wallpaperBitmap?.Dispose(); + _wallpaperBitmap = null; + _wallpaperVideoPath = importedPath; + _wallpaperMediaType = WallpaperMediaType.Video; + _wallpaperStatus = "Video wallpaper applied."; + break; + default: + _wallpaperStatus = "Selected file type is not supported."; + UpdateWallpaperDisplay(); + return; + } + + ApplyWallpaperBrush(); + UpdateWallpaperDisplay(); + RefreshColorPalettes(); + EnsureSelectedThemeColor(); + UpdateThemeColorSelectionState(); + ThemeColorStatusTextBlock.Text = _wallpaperMediaType == WallpaperMediaType.Video + ? "Video wallpaper updated. Theme colors refreshed." + : "Wallpaper updated. Monet colors refreshed."; + PersistSettings(); + } + catch (Exception ex) + { + _wallpaperStatus = $"Failed to apply wallpaper: {ex.Message}"; + UpdateWallpaperDisplay(); + } + } + + private void OnClearWallpaperClick(object? sender, RoutedEventArgs e) + { + StopVideoWallpaper(); + _wallpaperVideoPath = null; + _wallpaperMediaType = WallpaperMediaType.None; + _wallpaperBitmap?.Dispose(); + _wallpaperBitmap = null; + _wallpaperPath = null; + _wallpaperStatus = "Background reset to solid color."; + ApplyWallpaperBrush(); + UpdateWallpaperDisplay(); + RefreshColorPalettes(); + EnsureSelectedThemeColor(); + UpdateThemeColorSelectionState(); + ThemeColorStatusTextBlock.Text = "Wallpaper cleared. Monet colors refreshed."; + PersistSettings(); + } + + private void OnWallpaperPlacementSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + ApplyWallpaperBrush(); + if (_wallpaperMediaType == WallpaperMediaType.Image && _wallpaperBitmap is not null) + { + _wallpaperStatus = $"Wallpaper mode: {GetPlacementDisplayName(GetSelectedWallpaperPlacement())}."; + } + else if (_wallpaperMediaType == WallpaperMediaType.Video) + { + _wallpaperStatus = "Video wallpaper mode uses automatic fill."; + } + + UpdateWallpaperDisplay(); + PersistSettings(); + } + + private void ApplyWallpaperBrush() + { + if (_wallpaperMediaType == WallpaperMediaType.Video && + !string.IsNullOrWhiteSpace(_wallpaperVideoPath)) + { + DesktopWallpaperLayer.Background = Brushes.Transparent; + WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground(); + PlayVideoWallpaper(_wallpaperVideoPath); + return; + } + + StopVideoWallpaper(); + if (_wallpaperBitmap is null) + { + var fallbackBackground = GetThemeDefaultDesktopBackground(); + DesktopWallpaperLayer.Background = fallbackBackground; + WallpaperPreviewViewport.Background = fallbackBackground; + return; + } + + var placement = GetSelectedWallpaperPlacement(); + DesktopWallpaperLayer.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, false); + WallpaperPreviewViewport.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, true); + } + + private void UpdateWallpaperDisplay() + { + if (WallpaperPathTextBlock is null || + WallpaperStatusTextBlock is null || + WallpaperPreviewViewport is null || + WallpaperPlacementComboBox is null) + { + return; + } + + WallpaperPathTextBlock.Text = string.IsNullOrWhiteSpace(_wallpaperPath) + ? "No wallpaper selected." + : Path.GetFileName(_wallpaperPath); + WallpaperStatusTextBlock.Text = _wallpaperStatus; + WallpaperPlacementComboBox.IsEnabled = _wallpaperMediaType != WallpaperMediaType.Video; + + if (_wallpaperMediaType == WallpaperMediaType.Video) + { + WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground(); + return; + } + + if (_wallpaperBitmap is null) + { + WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground(); + return; + } + + WallpaperPreviewViewport.Background = CreateWallpaperBrush( + _wallpaperBitmap, + GetSelectedWallpaperPlacement(), + true); + } + + private ImageBrush CreateWallpaperBrush(Bitmap bitmap, WallpaperPlacement placement, bool forPreview) + { + var brush = new ImageBrush + { + Source = bitmap, + Stretch = Stretch.UniformToFill, + AlignmentX = AlignmentX.Center, + AlignmentY = AlignmentY.Center, + TileMode = TileMode.None + }; + + switch (placement) + { + case WallpaperPlacement.Fill: + brush.Stretch = Stretch.UniformToFill; + break; + case WallpaperPlacement.Fit: + brush.Stretch = Stretch.Uniform; + break; + case WallpaperPlacement.Stretch: + brush.Stretch = Stretch.Fill; + break; + case WallpaperPlacement.Center: + brush.Stretch = Stretch.None; + break; + case WallpaperPlacement.Tile: + brush.Stretch = Stretch.None; + brush.TileMode = TileMode.Tile; + var tileSize = forPreview ? 96d : 220d; + brush.DestinationRect = new RelativeRect(0, 0, tileSize, tileSize, RelativeUnit.Absolute); + break; + } + + return brush; + } + + private WallpaperPlacement GetSelectedWallpaperPlacement() + { + return WallpaperPlacementComboBox?.SelectedIndex switch + { + 1 => WallpaperPlacement.Fit, + 2 => WallpaperPlacement.Stretch, + 3 => WallpaperPlacement.Center, + 4 => WallpaperPlacement.Tile, + _ => WallpaperPlacement.Fill + }; + } + + private static string GetPlacementDisplayName(WallpaperPlacement placement) + { + return placement switch + { + WallpaperPlacement.Fill => "Fill", + WallpaperPlacement.Fit => "Fit", + WallpaperPlacement.Stretch => "Stretch", + WallpaperPlacement.Center => "Center", + WallpaperPlacement.Tile => "Tile", + _ => "Fill" + }; + } + + private IBrush GetThemeDefaultDesktopBackground() + { + if (Resources.TryGetResource("AdaptiveSurfaceBaseBrush", ActualThemeVariant, out var resource) && + resource is IBrush themedBrush) + { + return themedBrush; + } + + return _defaultDesktopBackground ?? + (_isNightMode + ? new SolidColorBrush(Color.Parse("#FF0B1220")) + : new SolidColorBrush(Color.Parse("#FFF3F7FB"))); + } + + private static int GetPlacementIndexFromSetting(string? placement) + { + if (string.IsNullOrWhiteSpace(placement)) + { + return 0; + } + + return placement.Trim().ToLowerInvariant() switch + { + "fit" => 1, + "stretch" => 2, + "center" => 3, + "tile" => 4, + _ => 0 + }; + } + + private void TryRestoreWallpaper(string? savedWallpaperPath) + { + StopVideoWallpaper(); + _wallpaperMediaType = WallpaperMediaType.None; + _wallpaperVideoPath = null; + _wallpaperBitmap?.Dispose(); + _wallpaperBitmap = null; + _wallpaperPath = null; + + if (string.IsNullOrWhiteSpace(savedWallpaperPath)) + { + _wallpaperStatus = "Current background uses solid color."; + return; + } + + if (!Path.IsPathRooted(savedWallpaperPath) || !File.Exists(savedWallpaperPath)) + { + _wallpaperStatus = "Saved wallpaper file was not found. Using solid color background."; + return; + } + + try + { + var mediaType = DetectWallpaperMediaType(savedWallpaperPath); + switch (mediaType) + { + case WallpaperMediaType.Image: + _wallpaperBitmap = new Bitmap(savedWallpaperPath); + _wallpaperPath = savedWallpaperPath; + _wallpaperMediaType = WallpaperMediaType.Image; + _wallpaperStatus = "Wallpaper restored from saved settings."; + break; + case WallpaperMediaType.Video: + _wallpaperVideoPath = savedWallpaperPath; + _wallpaperPath = savedWallpaperPath; + _wallpaperMediaType = WallpaperMediaType.Video; + _wallpaperStatus = "Video wallpaper restored from saved settings."; + break; + default: + _wallpaperStatus = "Saved wallpaper type is not supported. Using solid color background."; + break; + } + } + catch + { + _wallpaperStatus = "Failed to restore saved wallpaper. Using solid color background."; + _wallpaperBitmap?.Dispose(); + _wallpaperBitmap = null; + _wallpaperMediaType = WallpaperMediaType.None; + _wallpaperVideoPath = null; + _wallpaperPath = null; + } + } + + private static bool TryParseColor(string? colorText, out Color color) + { + color = default; + if (string.IsNullOrWhiteSpace(colorText)) + { + return false; + } + + try + { + color = Color.Parse(colorText); + return true; + } + catch + { + return false; + } + } + + private static WallpaperMediaType DetectWallpaperMediaType(string path) + { + var extension = Path.GetExtension(path); + if (string.IsNullOrWhiteSpace(extension)) + { + return WallpaperMediaType.None; + } + + if (SupportedImageExtensions.Contains(extension)) + { + return WallpaperMediaType.Image; + } + + if (SupportedVideoExtensions.Contains(extension)) + { + return WallpaperMediaType.Video; + } + + return WallpaperMediaType.None; + } + + private static async Task ImportWallpaperAssetAsync(IStorageFile file) + { + try + { + var extension = Path.GetExtension(file.Name); + if (string.IsNullOrWhiteSpace(extension)) + { + extension = ".bin"; + } + + var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var wallpaperDirectory = Path.Combine(appData, "LanMontainDesktop", "Wallpapers"); + Directory.CreateDirectory(wallpaperDirectory); + + var destinationPath = Path.Combine( + wallpaperDirectory, + $"{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}{extension}"); + + await using var sourceStream = await file.OpenReadAsync(); + await using var destinationStream = File.Create(destinationPath); + await sourceStream.CopyToAsync(destinationStream); + return destinationPath; + } + catch + { + return null; + } + } + + private void EnsureVideoWallpaperPlayers() + { + Core.Initialize(); + _libVlc ??= new LibVLC("--quiet"); + + if (_videoWallpaperPlayer is null && DesktopVideoWallpaperView is not null) + { + _videoWallpaperPlayer = new MediaPlayer(_libVlc); + DesktopVideoWallpaperView.MediaPlayer = _videoWallpaperPlayer; + } + + if (_previewVideoWallpaperPlayer is null && WallpaperPreviewVideoView is not null) + { + _previewVideoWallpaperPlayer = new MediaPlayer(_libVlc); + WallpaperPreviewVideoView.MediaPlayer = _previewVideoWallpaperPlayer; + } + } + + private void PlayVideoWallpaper(string videoPath) + { + if (!File.Exists(videoPath)) + { + _wallpaperStatus = "Video wallpaper file not found."; + StopVideoWallpaper(); + return; + } + + try + { + EnsureVideoWallpaperPlayers(); + if (_videoWallpaperPlayer is null || + _previewVideoWallpaperPlayer is null || + _libVlc is null || + DesktopVideoWallpaperView is null || + WallpaperPreviewVideoView is null) + { + _wallpaperStatus = "Video player is unavailable."; + StopVideoWallpaper(); + return; + } + + _videoWallpaperMedia?.Dispose(); + _previewVideoWallpaperMedia?.Dispose(); + _videoWallpaperMedia = new Media(_libVlc, new Uri(videoPath)); + _previewVideoWallpaperMedia = new Media(_libVlc, new Uri(videoPath)); + _videoWallpaperMedia.AddOption(":input-repeat=65535"); + _previewVideoWallpaperMedia.AddOption(":input-repeat=65535"); + _videoWallpaperPlayer.Play(_videoWallpaperMedia); + _previewVideoWallpaperPlayer.Play(_previewVideoWallpaperMedia); + DesktopVideoWallpaperView.IsVisible = true; + WallpaperPreviewVideoView.IsVisible = true; + } + catch (Exception ex) + { + _wallpaperStatus = $"Failed to play video wallpaper: {ex.Message}"; + StopVideoWallpaper(); + } + } + + private void StopVideoWallpaper() + { + if (DesktopVideoWallpaperView is not null) + { + DesktopVideoWallpaperView.IsVisible = false; + } + + if (WallpaperPreviewVideoView is not null) + { + WallpaperPreviewVideoView.IsVisible = false; + } + + if (_videoWallpaperPlayer?.IsPlaying == true) + { + _videoWallpaperPlayer.Stop(); + } + + if (_previewVideoWallpaperPlayer?.IsPlaying == true) + { + _previewVideoWallpaperPlayer.Stop(); + } + + _videoWallpaperMedia?.Dispose(); + _videoWallpaperMedia = null; + _previewVideoWallpaperMedia?.Dispose(); + _previewVideoWallpaperMedia = null; + } + + private void ApplyTaskbarSettings(AppSettingsSnapshot snapshot) + { + _topStatusComponentIds.Clear(); + if (snapshot.TopStatusComponentIds is not null) + { + foreach (var componentId in snapshot.TopStatusComponentIds) + { + if (!string.IsNullOrWhiteSpace(componentId)) + { + _topStatusComponentIds.Add(componentId.Trim()); + } + } + } + + _pinnedTaskbarActions.Clear(); + if (snapshot.PinnedTaskbarActions is not null) + { + foreach (var actionText in snapshot.PinnedTaskbarActions) + { + if (Enum.TryParse(actionText, ignoreCase: true, out var action)) + { + _pinnedTaskbarActions.Add(action); + } + } + } + + if (_pinnedTaskbarActions.Count == 0) + { + foreach (var action in DefaultPinnedTaskbarActions) + { + _pinnedTaskbarActions.Add(action); + } + } + + _enableDynamicTaskbarActions = snapshot.EnableDynamicTaskbarActions; + _taskbarLayoutMode = string.IsNullOrWhiteSpace(snapshot.TaskbarLayoutMode) + ? TaskbarLayoutBottomFullRowMacStyle + : snapshot.TaskbarLayoutMode; + } + + private void ApplyTopStatusComponentVisibility() + { + var showClock = _topStatusComponentIds.Contains(ClockStatusComponentId); + + if (ClockWidget is not null) + { + ClockWidget.IsVisible = showClock; + } + + if (WallpaperPreviewClockContainer is not null) + { + WallpaperPreviewClockContainer.IsVisible = showClock; + } + + if (WallpaperPreviewClockTextBlock is not null && showClock) + { + WallpaperPreviewClockTextBlock.Text = DateTime.Now.ToString("HH:mm"); + } + } + + private TaskbarContext GetCurrentTaskbarContext() + { + if (!_isSettingsOpen) + { + return TaskbarContext.Desktop; + } + + return SettingsNavListBox?.SelectedIndex switch + { + 0 => TaskbarContext.SettingsWallpaper, + 1 => TaskbarContext.SettingsGrid, + 2 => TaskbarContext.SettingsColor, + 3 => TaskbarContext.SettingsStatusBar, + _ => TaskbarContext.Desktop + }; + } + + private void ApplyTaskbarActionVisibility(TaskbarContext context) + { + if (BackToWindowsContainer is null || + OpenSettingsContainer is null || + WallpaperPreviewBackButtonContainer is null || + WallpaperPreviewSettingsButtonContainer is null) + { + return; + } + + var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows); + var showSettings = _pinnedTaskbarActions.Contains(TaskbarActionId.OpenSettings); + + BackToWindowsContainer.IsVisible = showMinimize; + OpenSettingsContainer.IsVisible = showSettings; + WallpaperPreviewBackButtonContainer.IsVisible = showMinimize; + WallpaperPreviewSettingsButtonContainer.IsVisible = showSettings; + + if (TaskbarFixedActionsHost is not null) + { + TaskbarFixedActionsHost.IsVisible = showMinimize || showSettings; + } + + if (WallpaperPreviewTaskbarFixedActionsHost is not null) + { + WallpaperPreviewTaskbarFixedActionsHost.IsVisible = showMinimize || showSettings; + } + + var dynamicActions = ResolveDynamicTaskbarActions(context); + var hasDynamicActions = dynamicActions.Count > 0; + BuildDynamicTaskbarVisuals(dynamicActions); + + if (TaskbarDynamicActionsHost is not null) + { + TaskbarDynamicActionsHost.IsVisible = hasDynamicActions; + } + + if (WallpaperPreviewTaskbarDynamicActionsHost is not null) + { + WallpaperPreviewTaskbarDynamicActionsHost.IsVisible = hasDynamicActions; + } + } + + private IReadOnlyList ResolveDynamicTaskbarActions(TaskbarContext context) + { + if (!_enableDynamicTaskbarActions) + { + return Array.Empty(); + } + + // Reserved for page-specific actions. Disabled by default in this phase. + _ = context; + return Array.Empty(); + } + + private void BuildDynamicTaskbarVisuals(IReadOnlyList actions) + { + if (TaskbarDynamicActionsPanel is not null) + { + TaskbarDynamicActionsPanel.Children.Clear(); + } + + if (WallpaperPreviewTaskbarDynamicActionsPanel is not null) + { + WallpaperPreviewTaskbarDynamicActionsPanel.Children.Clear(); + } + + if (actions.Count == 0 || + TaskbarDynamicActionsPanel is null || + WallpaperPreviewTaskbarDynamicActionsPanel is null) + { + return; + } + + foreach (var action in actions) + { + if (!action.IsVisible) + { + continue; + } + + var button = new Button + { + Content = action.Title, + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(12, 6), + Foreground = Foreground + }; + + TaskbarDynamicActionsPanel.Children.Add(button); + + var previewText = new TextBlock + { + Text = action.Title, + Foreground = Foreground, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center + }; + var previewBorder = new Border + { + Background = Brushes.Transparent, + BorderThickness = new Thickness(0), + Child = previewText + }; + WallpaperPreviewTaskbarDynamicActionsPanel.Children.Add(previewBorder); + } + } + + private void PersistSettings() + { + if (_suppressSettingsPersistence) + { + return; + } + + var snapshot = new AppSettingsSnapshot + { + GridShortSideCells = _targetShortSideCells, + IsNightMode = _isNightMode, + ThemeColor = _selectedThemeColor.ToString(), + WallpaperPath = _wallpaperPath, + WallpaperPlacement = GetPlacementDisplayName(GetSelectedWallpaperPlacement()), + SettingsTabIndex = Math.Max(0, SettingsNavListBox?.SelectedIndex ?? 0), + TopStatusComponentIds = _topStatusComponentIds.ToList(), + PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(), + EnableDynamicTaskbarActions = _enableDynamicTaskbarActions, + TaskbarLayoutMode = _taskbarLayoutMode + }; + + _appSettingsService.Save(snapshot); + } + + private void UpdateAdaptiveTextSystem() + { + var isLightBackground = _isSettingsOpen + ? !_isNightMode + : CalculateCurrentBackgroundLuminance() >= LightBackgroundLuminanceThreshold; + var isLightNavBackground = _isSettingsOpen + ? !_isNightMode + : CalculateBrushLuminance(SettingsNavPanelBorder?.Background) >= LightBackgroundLuminanceThreshold; + var context = new ThemeColorContext( + _selectedThemeColor, + isLightBackground, + isLightNavBackground, + _isNightMode); + + ThemeColorSystemService.ApplyThemeResources(Resources, context); + GlassEffectService.ApplyGlassResources(Resources, context); + if (_fluentAvaloniaTheme is not null) + { + _fluentAvaloniaTheme.CustomAccentColor = _selectedThemeColor; + } + } + + private double CalculateCurrentBackgroundLuminance() + { + if (_isSettingsOpen) + { + return CalculateBrushLuminance(SettingsContentPanel?.Background ?? SettingsPage?.Background); + } + + if (_wallpaperMediaType == WallpaperMediaType.Video) + { + return CalculateRelativeLuminance(Color.Parse("#FF0B1220")); + } + + if (_wallpaperBitmap is not null) + { + return CalculateBitmapAverageLuminance(_wallpaperBitmap); + } + + return CalculateBrushLuminance(DesktopWallpaperLayer.Background ?? _defaultDesktopBackground); + } + + private void ApplyNightModeState(bool enabled, bool refreshPalettes) + { + _isNightMode = enabled; + RequestedThemeVariant = enabled ? ThemeVariant.Dark : ThemeVariant.Light; + + _suppressThemeToggleEvents = true; + NightModeToggleSwitch.IsChecked = enabled; + _suppressThemeToggleEvents = false; + ThemeModeStatusTextBlock.Text = enabled ? "Night mode enabled" : "Day mode enabled"; + + if (refreshPalettes) + { + RefreshColorPalettes(); + EnsureSelectedThemeColor(); + } + + UpdateThemeColorSelectionState(); + ThemeColorStatusTextBlock.Text = $"Theme mode: {(enabled ? "Night" : "Day")}."; + UpdateAdaptiveTextSystem(); + ApplyWallpaperBrush(); + PersistSettings(); + } + + private void RefreshColorPalettes() + { + var palette = _monetColorService.BuildPalette(_wallpaperBitmap, _isNightMode); + _recommendedColors = palette.RecommendedColors; + _monetColors = palette.MonetColors; + ApplyColorPaletteToButtons(_recommendedColors, GetRecommendedColorTargets()); + ApplyColorPaletteToButtons(_monetColors, GetMonetColorTargets()); + } + + private void ApplyColorPaletteToButtons( + IReadOnlyList colors, + IReadOnlyList<(Button Button, Border Swatch)> targets) + { + for (var i = 0; i < targets.Count; i++) + { + var color = i < colors.Count + ? colors[i] + : Color.Parse("#00000000"); + var (button, swatch) = targets[i]; + button.Tag = color.ToString(); + button.IsEnabled = i < colors.Count; + swatch.Background = i < colors.Count + ? new SolidColorBrush(color) + : new SolidColorBrush(Color.Parse("#00000000")); + } + } + + private IReadOnlyList<(Button Button, Border Swatch)> GetRecommendedColorTargets() + { + return + [ + (RecommendedColorButton1, RecommendedColorSwatch1), + (RecommendedColorButton2, RecommendedColorSwatch2), + (RecommendedColorButton3, RecommendedColorSwatch3), + (RecommendedColorButton4, RecommendedColorSwatch4), + (RecommendedColorButton5, RecommendedColorSwatch5), + (RecommendedColorButton6, RecommendedColorSwatch6) + ]; + } + + private IReadOnlyList<(Button Button, Border Swatch)> GetMonetColorTargets() + { + return + [ + (MonetColorButton1, MonetColorSwatch1), + (MonetColorButton2, MonetColorSwatch2), + (MonetColorButton3, MonetColorSwatch3), + (MonetColorButton4, MonetColorSwatch4), + (MonetColorButton5, MonetColorSwatch5), + (MonetColorButton6, MonetColorSwatch6) + ]; + } + + private void EnsureSelectedThemeColor() + { + if (ContainsColor(_recommendedColors, _selectedThemeColor) || + ContainsColor(_monetColors, _selectedThemeColor)) + { + return; + } + + if (_recommendedColors.Count > 0) + { + _selectedThemeColor = _recommendedColors[0]; + return; + } + + if (_monetColors.Count > 0) + { + _selectedThemeColor = _monetColors[0]; + } + } + + private void ApplyThemeColorFromButton(Button? button, string sourceLabel) + { + if (!TryGetButtonColor(button, out var color)) + { + return; + } + + _selectedThemeColor = color; + UpdateThemeColorSelectionState(); + ThemeColorStatusTextBlock.Text = $"{sourceLabel} color applied: {_selectedThemeColor}."; + UpdateAdaptiveTextSystem(); + PersistSettings(); + } + + private void UpdateThemeColorSelectionState() + { + UpdateColorSelectionVisuals(GetRecommendedColorTargets()); + UpdateColorSelectionVisuals(GetMonetColorTargets()); + } + + private void UpdateColorSelectionVisuals(IReadOnlyList<(Button Button, Border Swatch)> targets) + { + foreach (var (button, swatch) in targets) + { + var isSelected = TryGetButtonColor(button, out var color) && AreSameColor(color, _selectedThemeColor); + button.Classes.Set("swatch-button", true); + button.Classes.Set("swatch-selected", isSelected); + swatch.BorderThickness = new Thickness(0); + swatch.Opacity = isSelected ? 1 : 0.9; + } + } + + private static bool TryGetButtonColor(Button? button, out Color color) + { + color = default; + if (button?.Tag is not string colorText || string.IsNullOrWhiteSpace(colorText)) + { + return false; + } + + try + { + color = Color.Parse(colorText); + return true; + } + catch + { + return false; + } + } + + + private static bool ContainsColor(IReadOnlyList colors, Color target) + { + for (var i = 0; i < colors.Count; i++) + { + if (AreSameColor(colors[i], target)) + { + return true; + } + } + + return false; + } + + private static bool AreSameColor(Color left, Color right) + { + return left.R == right.R && left.G == right.G && left.B == right.B; + } + + + private static double CalculateBrushLuminance(IBrush? brush) + { + if (brush is ISolidColorBrush solidBrush) + { + return CalculateRelativeLuminance(solidBrush.Color); + } + + return CalculateRelativeLuminance(Color.Parse("#FF020617")); + } + + private static double CalculateBitmapAverageLuminance(Bitmap bitmap) + { + try + { + var sampleWidth = Math.Clamp(bitmap.PixelSize.Width, 1, 48); + var sampleHeight = Math.Clamp(bitmap.PixelSize.Height, 1, 48); + + using var scaledBitmap = bitmap.CreateScaledBitmap( + new PixelSize(sampleWidth, sampleHeight), + BitmapInterpolationMode.MediumQuality); + using var writeable = new WriteableBitmap( + scaledBitmap.PixelSize, + new Vector(96, 96), + PixelFormat.Bgra8888, + AlphaFormat.Premul); + using var framebuffer = writeable.Lock(); + + scaledBitmap.CopyPixels(framebuffer, AlphaFormat.Premul); + + var rowBytes = framebuffer.RowBytes; + var byteCount = rowBytes * framebuffer.Size.Height; + if (byteCount <= 0 || framebuffer.Address == IntPtr.Zero) + { + return CalculateRelativeLuminance(Color.Parse("#FF020617")); + } + + var pixelBuffer = new byte[byteCount]; + Marshal.Copy(framebuffer.Address, pixelBuffer, 0, byteCount); + + double luminanceSum = 0; + var pixelCount = 0; + for (var y = 0; y < framebuffer.Size.Height; y++) + { + var rowOffset = y * rowBytes; + for (var x = 0; x < framebuffer.Size.Width; x++) + { + var index = rowOffset + (x * 4); + var alpha = pixelBuffer[index + 3] / 255d; + if (alpha <= 0.01) + { + continue; + } + + var blue = (pixelBuffer[index] / 255d) / alpha; + var green = (pixelBuffer[index + 1] / 255d) / alpha; + var red = (pixelBuffer[index + 2] / 255d) / alpha; + + red = Math.Clamp(red, 0, 1); + green = Math.Clamp(green, 0, 1); + blue = Math.Clamp(blue, 0, 1); + + luminanceSum += CalculateRelativeLuminance(red, green, blue); + pixelCount++; + } + } + + return pixelCount > 0 + ? luminanceSum / pixelCount + : CalculateRelativeLuminance(Color.Parse("#FF020617")); + } + catch + { + return CalculateRelativeLuminance(Color.Parse("#FF020617")); + } + } + + private static double CalculateRelativeLuminance(Color color) + { + return CalculateRelativeLuminance(color.R / 255d, color.G / 255d, color.B / 255d); + } + + private static double CalculateRelativeLuminance(double red, double green, double blue) + { + var linearRed = ToLinearRgb(red); + var linearGreen = ToLinearRgb(green); + var linearBlue = ToLinearRgb(blue); + return (0.2126 * linearRed) + (0.7152 * linearGreen) + (0.0722 * linearBlue); + } + + private static double ToLinearRgb(double value) + { + return value <= 0.04045 + ? value / 12.92 + : Math.Pow((value + 0.055) / 1.055, 2.4); + } + + private void OpenSettingsPage() + { + if (_isSettingsOpen) + { + return; + } + + _isSettingsOpen = true; + UpdateAdaptiveTextSystem(); + ApplyWallpaperBrush(); + ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); + SettingsPage.IsVisible = true; + SettingsPage.Opacity = 0; + if (_settingsContentPanelTransform is not null) + { + _settingsContentPanelTransform.Y = 30; + } + + DesktopPage.IsHitTestVisible = false; + UpdateWallpaperPreviewLayout(); + + Dispatcher.UIThread.Post(() => + { + if (!_isSettingsOpen) + { + return; + } + + SettingsPage.Opacity = 1; + if (_settingsContentPanelTransform is not null) + { + _settingsContentPanelTransform.Y = 0; + } + }, DispatcherPriority.Background); + } + + private void CloseSettingsPage() + { + if (!_isSettingsOpen) + { + return; + } + + _isSettingsOpen = false; + UpdateAdaptiveTextSystem(); + ApplyWallpaperBrush(); + ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); + + DesktopPage.IsHitTestVisible = true; + + SettingsPage.Opacity = 0; + if (_settingsContentPanelTransform is not null) + { + _settingsContentPanelTransform.Y = 30; + } + + DispatcherTimer.RunOnce(() => + { + if (_isSettingsOpen) + { + return; + } + + SettingsPage.IsVisible = false; + }, TimeSpan.FromMilliseconds(SettingsTransitionDurationMs)); + } +} diff --git a/LanMontainDesktop/Views/MainWindow.axaml b/LanMontainDesktop/Views/MainWindow.axaml index 7883cd8..54373ac 100644 --- a/LanMontainDesktop/Views/MainWindow.axaml +++ b/LanMontainDesktop/Views/MainWindow.axaml @@ -1,8 +1,10 @@ - @@ -23,10 +26,17 @@ + + + + + + + @@ -35,17 +45,22 @@ + + + + + + 22 + 28 + 0.92 + 0.95 - - - - @@ -56,46 +71,117 @@ - - - - - + + + + + + + + + + + + + + @@ -110,8 +196,7 @@ + Classes="glass-overlay" /> + @@ -184,6 +272,7 @@ + @@ -212,60 +301,109 @@ Width="360" Height="220" CornerRadius="14" - BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}" - BorderThickness="1" Background="#22000000"> - - + + + + - + BorderThickness="0" + Padding="2"> + + + + + - - - + CornerRadius="8" + Padding="2"> + + + + + + + + + - - + + + + + + + + + + + @@ -406,8 +544,7 @@ Width="26" Height="26" CornerRadius="6" - BorderBrush="#A0FFFFFF" - BorderThickness="1" /> + BorderThickness="0" /> @@ -494,8 +626,7 @@ Width="26" Height="26" CornerRadius="6" - BorderBrush="#A0FFFFFF" - BorderThickness="1" /> + BorderThickness="0" /> @@ -563,6 +689,33 @@ Foreground="{DynamicResource AdaptiveTextMutedBrush}" Text="Theme color is ready." /> + + + + + + + + + + + + @@ -582,3 +735,6 @@ + + + diff --git a/LanMontainDesktop/Views/MainWindow.axaml.cs b/LanMontainDesktop/Views/MainWindow.axaml.cs index dd38747..9b1cbd4 100644 --- a/LanMontainDesktop/Views/MainWindow.axaml.cs +++ b/LanMontainDesktop/Views/MainWindow.axaml.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Runtime.InteropServices; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; @@ -10,9 +13,11 @@ using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Styling; using Avalonia.Threading; +using FluentAvalonia.Styling; using LanMontainDesktop.Models; using LanMontainDesktop.Services; using LanMontainDesktop.Theme; +using LibVLCSharp.Shared; namespace LanMontainDesktop.Views; @@ -27,32 +32,67 @@ public partial class MainWindow : Window Tile } + private enum WallpaperMediaType + { + None, + Image, + Video + } + private const int StatusBarRowIndex = 0; private const int MinShortSideCells = 6; private const int MaxShortSideCells = 96; private const int SettingsTransitionDurationMs = 240; private const double LightBackgroundLuminanceThreshold = 0.57; - private const double WallpaperPreviewMinWidth = 220; - private const double WallpaperPreviewMinHeight = 140; - private const double WallpaperPreviewMaxHeight = 280; + private const string ClockStatusComponentId = "Clock"; + private const string TaskbarLayoutBottomFullRowMacStyle = "BottomFullRowMacStyle"; + private static readonly HashSet SupportedImageExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp" + }; + private static readonly HashSet SupportedVideoExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v" + }; + private static readonly TaskbarActionId[] DefaultPinnedTaskbarActions = + [ + TaskbarActionId.MinimizeToWindows, + TaskbarActionId.OpenSettings + ]; private readonly record struct GridMetrics(int ColumnCount, int RowCount, double CellSize); private readonly MonetColorService _monetColorService = new(); + private readonly AppSettingsService _appSettingsService = new(); + private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme; + private readonly HashSet _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _pinnedTaskbarActions = []; private int _targetShortSideCells; private bool _isSettingsOpen; private bool _isNightMode; + private bool _enableDynamicTaskbarActions; private bool _suppressThemeToggleEvents; + private bool _suppressStatusBarToggleEvents; + private bool _suppressSettingsPersistence; private TranslateTransform? _settingsContentPanelTransform; private IBrush? _defaultDesktopBackground; private Bitmap? _wallpaperBitmap; + private WallpaperMediaType _wallpaperMediaType; + private string? _wallpaperVideoPath; + private LibVLC? _libVlc; + private MediaPlayer? _videoWallpaperPlayer; + private Media? _videoWallpaperMedia; + private MediaPlayer? _previewVideoWallpaperPlayer; + private Media? _previewVideoWallpaperMedia; private string? _wallpaperPath; private string _wallpaperStatus = "Current background uses solid color."; private IReadOnlyList _recommendedColors = Array.Empty(); private IReadOnlyList _monetColors = Array.Empty(); private Color _selectedThemeColor = Color.Parse("#FF3B82F6"); + private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle; public MainWindow() { InitializeComponent(); + _fluentAvaloniaTheme = Application.Current?.Styles.OfType().FirstOrDefault(); PropertyChanged += OnWindowPropertyChanged; } @@ -60,28 +100,60 @@ public partial class MainWindow : Window { base.OnOpened(e); - _targetShortSideCells = CalculateDefaultShortSideCellCountFromDpi(); + _suppressSettingsPersistence = true; + var snapshot = _appSettingsService.Load(); + + _targetShortSideCells = Math.Clamp( + snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(), + MinShortSideCells, + MaxShortSideCells); GridSizeNumberBox.Value = _targetShortSideCells; - SettingsNavListBox.SelectedIndex = 0; + + SettingsNavListBox.SelectedIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, 3); UpdateSettingsTabContent(); - WallpaperPlacementComboBox.SelectedIndex = 0; - _defaultDesktopBackground = DesktopHost.Background; + + WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement); + _defaultDesktopBackground = DesktopWallpaperLayer.Background; + ApplyTaskbarSettings(snapshot); + + TryRestoreWallpaper(snapshot.WallpaperPath); + ApplyWallpaperBrush(); UpdateWallpaperDisplay(); - _isNightMode = CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold; - ApplyNightModeState(_isNightMode, refreshPalettes: false); - RefreshColorPalettes(); - EnsureSelectedThemeColor(); - UpdateThemeColorSelectionState(); + + if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor)) + { + _selectedThemeColor = savedThemeColor; + } + + _isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold); + ApplyNightModeState(_isNightMode, refreshPalettes: true); + _suppressStatusBarToggleEvents = true; + StatusBarClockToggleSwitch.IsChecked = _topStatusComponentIds.Contains(ClockStatusComponentId); + _suppressStatusBarToggleEvents = false; ThemeColorStatusTextBlock.Text = $"Theme color ready: {_selectedThemeColor}."; - UpdateAdaptiveTextSystem(); _settingsContentPanelTransform = SettingsContentPanel.RenderTransform as TranslateTransform; DesktopHost.SizeChanged += OnDesktopHostSizeChanged; WallpaperPreviewHost.SizeChanged += OnWallpaperPreviewHostSizeChanged; RebuildDesktopGrid(); + + _suppressSettingsPersistence = false; + PersistSettings(); } protected override void OnClosed(EventArgs e) { + PersistSettings(); + StopVideoWallpaper(); + _previewVideoWallpaperMedia?.Dispose(); + _previewVideoWallpaperMedia = null; + _previewVideoWallpaperPlayer?.Dispose(); + _previewVideoWallpaperPlayer = null; + _videoWallpaperMedia?.Dispose(); + _videoWallpaperMedia = null; + _videoWallpaperPlayer?.Dispose(); + _videoWallpaperPlayer = null; + _libVlc?.Dispose(); + _libVlc = null; _wallpaperBitmap?.Dispose(); _wallpaperBitmap = null; PropertyChanged -= OnWindowPropertyChanged; @@ -100,6 +172,7 @@ public partial class MainWindow : Window private void OnDesktopHostSizeChanged(object? sender, SizeChangedEventArgs e) { RebuildDesktopGrid(); + PersistSettings(); } private void OnWallpaperPreviewHostSizeChanged(object? sender, SizeChangedEventArgs e) @@ -151,37 +224,20 @@ public partial class MainWindow : Window DesktopGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); } - PlaceStatusBarComponent(ClockWidget, column: 0, requestedColumnSpan: 3, totalColumns: gridMetrics.ColumnCount); + PlaceStatusBarComponent( + TopStatusBarHost, + column: 0, + requestedColumnSpan: gridMetrics.ColumnCount, + totalColumns: gridMetrics.ColumnCount); - var firstDesktopRow = Math.Min(gridMetrics.RowCount - 1, StatusBarRowIndex + 1); + var taskbarRow = gridMetrics.RowCount - 1; + Grid.SetRow(BottomTaskbarContainer, taskbarRow); + Grid.SetColumn(BottomTaskbarContainer, 0); + Grid.SetRowSpan(BottomTaskbarContainer, 1); + Grid.SetColumnSpan(BottomTaskbarContainer, gridMetrics.ColumnCount); - var settingsColumnSpan = ClampComponentSpan(2, gridMetrics.ColumnCount); - var settingsRowSpan = ClampComponentSpan(1, gridMetrics.RowCount); - var settingsRow = Math.Max(firstDesktopRow, gridMetrics.RowCount - 1); - var settingsColumn = Math.Max(0, gridMetrics.ColumnCount - settingsColumnSpan); - - var backButtonRow = settingsRow; - var backButtonMaxColumnsWithoutOverlap = settingsColumn; - int backButtonColumnSpan; - if (backButtonMaxColumnsWithoutOverlap >= 1) - { - backButtonColumnSpan = ClampComponentSpan(Math.Min(4, backButtonMaxColumnsWithoutOverlap), gridMetrics.ColumnCount); - } - else - { - backButtonRow = Math.Max(firstDesktopRow, gridMetrics.RowCount - 2); - backButtonColumnSpan = ClampComponentSpan(Math.Min(4, gridMetrics.ColumnCount), gridMetrics.ColumnCount); - } - - Grid.SetRow(BackToWindowsContainer, backButtonRow); - Grid.SetColumn(BackToWindowsContainer, 0); - Grid.SetRowSpan(BackToWindowsContainer, ClampComponentSpan(1, gridMetrics.RowCount)); - Grid.SetColumnSpan(BackToWindowsContainer, backButtonColumnSpan); - - Grid.SetRow(OpenSettingsButton, settingsRow); - Grid.SetColumn(OpenSettingsButton, settingsColumn); - Grid.SetRowSpan(OpenSettingsButton, settingsRowSpan); - Grid.SetColumnSpan(OpenSettingsButton, settingsColumnSpan); + ApplyTopStatusComponentVisibility(); + ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); ApplyWidgetSizing(gridMetrics.CellSize); @@ -242,20 +298,30 @@ public partial class MainWindow : Window var margin = Math.Clamp(cellSize * 0.08, 1.5, 10); var verticalPadding = Math.Clamp(cellSize * 0.08, 2, 12); var horizontalPadding = Math.Clamp(cellSize * 0.20, 4, 22); + var taskbarCell = Math.Clamp(cellSize, 28, 128); + TopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1.5, 10)); ClockWidget.Margin = new Thickness(margin); ClockWidget.ApplyCellSize(cellSize); - BackToWindowsContainer.Margin = new Thickness(margin); - BackToWindowsContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.12, 5, 14)); - BackToWindowsButton.Padding = new Thickness(horizontalPadding, verticalPadding); - BackToWindowsButton.FontSize = Math.Clamp(cellSize * 0.30, 8, 30); + BottomTaskbarContainer.Margin = new Thickness(Math.Clamp(cellSize * 0.18, 6, 18)); + BottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.24, 10, 24)); + BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 2, 10)); - OpenSettingsButton.Margin = new Thickness(Math.Clamp(cellSize * 0.12, 6, 16)); - OpenSettingsButton.Padding = new Thickness( - Math.Clamp(horizontalPadding + 2, 8, 26), - Math.Clamp(verticalPadding, 4, 12)); - OpenSettingsButton.FontSize = Math.Clamp(cellSize * 0.22, 9, 22); + BackToWindowsContainer.Margin = new Thickness(0); + BackToWindowsContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.16, 6, 16)); + BackToWindowsButton.Padding = new Thickness(horizontalPadding, verticalPadding); + BackToWindowsButton.FontSize = Math.Clamp(cellSize * 0.22, 8, 22); + BackToWindowsButton.MinHeight = taskbarCell; + BackToWindowsButton.MinWidth = Math.Clamp(cellSize * 2.3, 90, 320); + + OpenSettingsContainer.Margin = new Thickness(0); + OpenSettingsContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.16, 6, 16)); + OpenSettingsContainer.Width = taskbarCell; + OpenSettingsContainer.Height = taskbarCell; + OpenSettingsButton.Width = taskbarCell; + OpenSettingsButton.Height = taskbarCell; + OpenSettingsButton.Padding = new Thickness(Math.Clamp(taskbarCell * 0.2, 4, 12)); } private void UpdateWallpaperPreviewLayout() @@ -270,24 +336,24 @@ public partial class MainWindow : Window var desktopWidth = Math.Max(1, DesktopHost.Bounds.Width); var desktopHeight = Math.Max(1, DesktopHost.Bounds.Height); var aspectRatio = desktopWidth / desktopHeight; - var availableWidth = WallpaperPreviewHost.Bounds.Width - 24; + var availableWidth = WallpaperPreviewHost.Bounds.Width - 20; + var availableHeight = WallpaperPreviewHost.Bounds.Height - 20; if (availableWidth <= 1) { availableWidth = WallpaperPreviewFrame.Width; } - - var previewWidth = Math.Max(WallpaperPreviewMinWidth, availableWidth); - var previewHeight = previewWidth / aspectRatio; - - if (previewHeight > WallpaperPreviewMaxHeight) + if (availableHeight <= 1) { - previewHeight = WallpaperPreviewMaxHeight; - previewWidth = previewHeight * aspectRatio; + availableHeight = WallpaperPreviewFrame.Height; } + availableWidth = Math.Max(1, availableWidth); + availableHeight = Math.Max(1, availableHeight); - if (previewHeight < WallpaperPreviewMinHeight) + var previewWidth = availableWidth; + var previewHeight = previewWidth / aspectRatio; + if (previewHeight > availableHeight) { - previewHeight = WallpaperPreviewMinHeight; + previewHeight = availableHeight; previewWidth = previewHeight * aspectRatio; } @@ -319,52 +385,39 @@ public partial class MainWindow : Window } PlaceStatusBarComponent( - WallpaperPreviewClockContainer, + WallpaperPreviewTopStatusBarHost, column: 0, - requestedColumnSpan: 3, + requestedColumnSpan: gridMetrics.ColumnCount, totalColumns: gridMetrics.ColumnCount); - var firstDesktopRow = Math.Min(gridMetrics.RowCount - 1, StatusBarRowIndex + 1); - var settingsColumnSpan = ClampComponentSpan(2, gridMetrics.ColumnCount); - var settingsRow = Math.Max(firstDesktopRow, gridMetrics.RowCount - 1); - var settingsColumn = Math.Max(0, gridMetrics.ColumnCount - settingsColumnSpan); - - var backButtonRow = settingsRow; - var backButtonMaxColumnsWithoutOverlap = settingsColumn; - int backButtonColumnSpan; - if (backButtonMaxColumnsWithoutOverlap >= 1) - { - backButtonColumnSpan = ClampComponentSpan(Math.Min(4, backButtonMaxColumnsWithoutOverlap), gridMetrics.ColumnCount); - } - else - { - backButtonRow = Math.Max(firstDesktopRow, gridMetrics.RowCount - 2); - backButtonColumnSpan = ClampComponentSpan(Math.Min(4, gridMetrics.ColumnCount), gridMetrics.ColumnCount); - } - - Grid.SetRow(WallpaperPreviewBackButtonContainer, backButtonRow); - Grid.SetColumn(WallpaperPreviewBackButtonContainer, 0); - Grid.SetRowSpan(WallpaperPreviewBackButtonContainer, 1); - Grid.SetColumnSpan(WallpaperPreviewBackButtonContainer, backButtonColumnSpan); - - Grid.SetRow(WallpaperPreviewSettingsButtonContainer, settingsRow); - Grid.SetColumn(WallpaperPreviewSettingsButtonContainer, settingsColumn); - Grid.SetRowSpan(WallpaperPreviewSettingsButtonContainer, 1); - Grid.SetColumnSpan(WallpaperPreviewSettingsButtonContainer, settingsColumnSpan); + var taskbarRow = gridMetrics.RowCount - 1; + Grid.SetRow(WallpaperPreviewBottomTaskbarContainer, taskbarRow); + Grid.SetColumn(WallpaperPreviewBottomTaskbarContainer, 0); + Grid.SetRowSpan(WallpaperPreviewBottomTaskbarContainer, 1); + Grid.SetColumnSpan(WallpaperPreviewBottomTaskbarContainer, gridMetrics.ColumnCount); + ApplyTopStatusComponentVisibility(); + ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); ApplyPreviewWidgetSizing(gridMetrics.CellSize); } private void ApplyPreviewWidgetSizing(double cellSize) { var margin = Math.Clamp(cellSize * 0.08, 1, 6); - WallpaperPreviewClockContainer.Margin = new Thickness(margin); - WallpaperPreviewBackButtonContainer.Margin = new Thickness(margin); - WallpaperPreviewSettingsButtonContainer.Margin = new Thickness(margin); + var previewTaskbarCell = Math.Clamp(cellSize, 10, 36); + WallpaperPreviewTopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1, 4)); + WallpaperPreviewBottomTaskbarContainer.Margin = new Thickness(margin); + WallpaperPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.22, 4, 10)); + WallpaperPreviewBottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 1, 4)); WallpaperPreviewClockTextBlock.FontSize = Math.Clamp(cellSize * 0.30, 6, 18); WallpaperPreviewBackButtonTextBlock.FontSize = Math.Clamp(cellSize * 0.19, 5, 13); - WallpaperPreviewSettingsButtonTextBlock.FontSize = Math.Clamp(cellSize * 0.19, 5, 13); + WallpaperPreviewBackButtonContainer.MinHeight = previewTaskbarCell; + WallpaperPreviewBackButtonContainer.MinWidth = Math.Clamp(cellSize * 2.1, 30, 120); + WallpaperPreviewSettingsButtonContainer.Width = previewTaskbarCell; + WallpaperPreviewSettingsButtonContainer.Height = previewTaskbarCell; + WallpaperPreviewSettingsButtonIcon.Width = Math.Clamp(previewTaskbarCell * 0.42, 6, 14); + WallpaperPreviewSettingsButtonIcon.Height = Math.Clamp(previewTaskbarCell * 0.42, 6, 14); var cornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.12, 3, 10)); WallpaperPreviewBackButtonContainer.CornerRadius = cornerRadius; @@ -376,617 +429,6 @@ public partial class MainWindow : Window WindowState = WindowState.Minimized; } - private void OnOpenSettingsClick(object? sender, RoutedEventArgs e) - { - OpenSettingsPage(); - } - - private void OnCloseSettingsClick(object? sender, RoutedEventArgs e) - { - CloseSettingsPage(); - } - - private void OnSettingsNavSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - UpdateSettingsTabContent(); - } - - private void UpdateSettingsTabContent() - { - // SelectionChanged can fire during XAML initialization before all named controls are assigned. - if (SettingsNavListBox is null || - GridSettingsPanel is null || - WallpaperSettingsPanel is null || - ColorSettingsPanel is null) - { - return; - } - - var selectedIndex = SettingsNavListBox.SelectedIndex; - WallpaperSettingsPanel.IsVisible = selectedIndex == 0; - GridSettingsPanel.IsVisible = selectedIndex == 1; - ColorSettingsPanel.IsVisible = selectedIndex == 2; - } - - private void OnNightModeChecked(object? sender, RoutedEventArgs e) - { - if (_suppressThemeToggleEvents) - { - return; - } - - ApplyNightModeState(true, refreshPalettes: true); - } - - private void OnNightModeUnchecked(object? sender, RoutedEventArgs e) - { - if (_suppressThemeToggleEvents) - { - return; - } - - ApplyNightModeState(false, refreshPalettes: true); - } - - private void OnRecommendedColorClick(object? sender, RoutedEventArgs e) - { - ApplyThemeColorFromButton(sender as Button, "Recommended"); - } - - private void OnMonetColorClick(object? sender, RoutedEventArgs e) - { - ApplyThemeColorFromButton(sender as Button, "Monet"); - } - - private void OnRefreshMonetColorsClick(object? sender, RoutedEventArgs e) - { - RefreshColorPalettes(); - EnsureSelectedThemeColor(); - UpdateThemeColorSelectionState(); - ThemeColorStatusTextBlock.Text = "Monet colors refreshed."; - UpdateAdaptiveTextSystem(); - } - - private async void OnPickWallpaperClick(object? sender, RoutedEventArgs e) - { - if (StorageProvider is null) - { - _wallpaperStatus = "Storage provider is unavailable."; - UpdateWallpaperDisplay(); - return; - } - - var options = new FilePickerOpenOptions - { - Title = "Select wallpaper", - AllowMultiple = false, - FileTypeFilter = - [ - new FilePickerFileType("Image files") - { - Patterns = ["*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif", "*.webp"] - } - ] - }; - - var files = await StorageProvider.OpenFilePickerAsync(options); - if (files.Count == 0) - { - return; - } - - var file = files[0]; - try - { - Bitmap bitmap; - var localPath = file.TryGetLocalPath(); - if (!string.IsNullOrWhiteSpace(localPath)) - { - bitmap = new Bitmap(localPath); - _wallpaperPath = localPath; - } - else - { - await using var stream = await file.OpenReadAsync(); - bitmap = new Bitmap(stream); - _wallpaperPath = file.Name; - } - - _wallpaperBitmap?.Dispose(); - _wallpaperBitmap = bitmap; - _wallpaperStatus = "Wallpaper applied."; - ApplyWallpaperBrush(); - UpdateWallpaperDisplay(); - RefreshColorPalettes(); - EnsureSelectedThemeColor(); - UpdateThemeColorSelectionState(); - ThemeColorStatusTextBlock.Text = "Wallpaper updated. Monet colors refreshed."; - } - catch (Exception ex) - { - _wallpaperStatus = $"Failed to apply wallpaper: {ex.Message}"; - UpdateWallpaperDisplay(); - } - } - - private void OnClearWallpaperClick(object? sender, RoutedEventArgs e) - { - _wallpaperBitmap?.Dispose(); - _wallpaperBitmap = null; - _wallpaperPath = null; - _wallpaperStatus = "Background reset to solid color."; - ApplyWallpaperBrush(); - UpdateWallpaperDisplay(); - RefreshColorPalettes(); - EnsureSelectedThemeColor(); - UpdateThemeColorSelectionState(); - ThemeColorStatusTextBlock.Text = "Wallpaper cleared. Monet colors refreshed."; - } - - private void OnWallpaperPlacementSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - ApplyWallpaperBrush(); - if (_wallpaperBitmap is not null) - { - _wallpaperStatus = $"Wallpaper mode: {GetPlacementDisplayName(GetSelectedWallpaperPlacement())}."; - } - - UpdateWallpaperDisplay(); - } - - private void ApplyWallpaperBrush() - { - if (_wallpaperBitmap is null) - { - DesktopHost.Background = _defaultDesktopBackground ?? new SolidColorBrush(Color.Parse("#FF020617")); - WallpaperPreviewViewport.Background = _defaultDesktopBackground ?? new SolidColorBrush(Color.Parse("#30111827")); - UpdateAdaptiveTextSystem(); - return; - } - - var placement = GetSelectedWallpaperPlacement(); - DesktopHost.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, false); - WallpaperPreviewViewport.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, true); - UpdateAdaptiveTextSystem(); - } - - private void UpdateWallpaperDisplay() - { - if (WallpaperPathTextBlock is null || - WallpaperStatusTextBlock is null || - WallpaperPreviewViewport is null || - WallpaperPlacementComboBox is null) - { - return; - } - - WallpaperPathTextBlock.Text = string.IsNullOrWhiteSpace(_wallpaperPath) - ? "No wallpaper selected." - : _wallpaperPath; - WallpaperStatusTextBlock.Text = _wallpaperStatus; - - if (_wallpaperBitmap is null) - { - WallpaperPreviewViewport.Background = _defaultDesktopBackground ?? new SolidColorBrush(Color.Parse("#30111827")); - return; - } - - WallpaperPreviewViewport.Background = CreateWallpaperBrush( - _wallpaperBitmap, - GetSelectedWallpaperPlacement(), - true); - } - - private ImageBrush CreateWallpaperBrush(Bitmap bitmap, WallpaperPlacement placement, bool forPreview) - { - var brush = new ImageBrush - { - Source = bitmap, - Stretch = Stretch.UniformToFill, - AlignmentX = AlignmentX.Center, - AlignmentY = AlignmentY.Center, - TileMode = TileMode.None - }; - - switch (placement) - { - case WallpaperPlacement.Fill: - brush.Stretch = Stretch.UniformToFill; - break; - case WallpaperPlacement.Fit: - brush.Stretch = Stretch.Uniform; - break; - case WallpaperPlacement.Stretch: - brush.Stretch = Stretch.Fill; - break; - case WallpaperPlacement.Center: - brush.Stretch = Stretch.None; - break; - case WallpaperPlacement.Tile: - brush.Stretch = Stretch.None; - brush.TileMode = TileMode.Tile; - var tileSize = forPreview ? 96d : 220d; - brush.DestinationRect = new RelativeRect(0, 0, tileSize, tileSize, RelativeUnit.Absolute); - break; - } - - return brush; - } - - private WallpaperPlacement GetSelectedWallpaperPlacement() - { - return WallpaperPlacementComboBox?.SelectedIndex switch - { - 1 => WallpaperPlacement.Fit, - 2 => WallpaperPlacement.Stretch, - 3 => WallpaperPlacement.Center, - 4 => WallpaperPlacement.Tile, - _ => WallpaperPlacement.Fill - }; - } - - private static string GetPlacementDisplayName(WallpaperPlacement placement) - { - return placement switch - { - WallpaperPlacement.Fill => "Fill", - WallpaperPlacement.Fit => "Fit", - WallpaperPlacement.Stretch => "Stretch", - WallpaperPlacement.Center => "Center", - WallpaperPlacement.Tile => "Tile", - _ => "Fill" - }; - } - - private void UpdateAdaptiveTextSystem() - { - var luminance = CalculateCurrentBackgroundLuminance(); - var isLightBackground = luminance >= LightBackgroundLuminanceThreshold; - var navBackground = SettingsNavPanelBorder?.Background; - var isLightNavBackground = CalculateBrushLuminance(navBackground) >= LightBackgroundLuminanceThreshold; - var context = new ThemeColorContext( - _selectedThemeColor, - isLightBackground, - isLightNavBackground, - _isNightMode); - - ThemeColorSystemService.ApplyThemeResources(Resources, context); - GlassEffectService.ApplyGlassResources(Resources, context.IsLightBackground); - } - - private double CalculateCurrentBackgroundLuminance() - { - if (_wallpaperBitmap is not null) - { - return CalculateBitmapAverageLuminance(_wallpaperBitmap); - } - - return CalculateBrushLuminance(DesktopHost.Background ?? _defaultDesktopBackground); - } - - private void ApplyNightModeState(bool enabled, bool refreshPalettes) - { - _isNightMode = enabled; - RequestedThemeVariant = enabled ? ThemeVariant.Dark : ThemeVariant.Light; - - _suppressThemeToggleEvents = true; - NightModeToggleSwitch.IsChecked = enabled; - _suppressThemeToggleEvents = false; - ThemeModeStatusTextBlock.Text = enabled ? "Night mode enabled" : "Day mode enabled"; - - if (refreshPalettes) - { - RefreshColorPalettes(); - EnsureSelectedThemeColor(); - } - - UpdateThemeColorSelectionState(); - ThemeColorStatusTextBlock.Text = $"Theme mode: {(enabled ? "Night" : "Day")}."; - UpdateAdaptiveTextSystem(); - } - - private void RefreshColorPalettes() - { - var palette = _monetColorService.BuildPalette(_wallpaperBitmap, _isNightMode); - _recommendedColors = palette.RecommendedColors; - _monetColors = palette.MonetColors; - ApplyColorPaletteToButtons(_recommendedColors, GetRecommendedColorTargets()); - ApplyColorPaletteToButtons(_monetColors, GetMonetColorTargets()); - } - - private void ApplyColorPaletteToButtons( - IReadOnlyList colors, - IReadOnlyList<(Button Button, Border Swatch)> targets) - { - for (var i = 0; i < targets.Count; i++) - { - var color = i < colors.Count - ? colors[i] - : Color.Parse("#00000000"); - var (button, swatch) = targets[i]; - button.Tag = color.ToString(); - button.IsEnabled = i < colors.Count; - swatch.Background = i < colors.Count - ? new SolidColorBrush(color) - : new SolidColorBrush(Color.Parse("#00000000")); - } - } - - private IReadOnlyList<(Button Button, Border Swatch)> GetRecommendedColorTargets() - { - return - [ - (RecommendedColorButton1, RecommendedColorSwatch1), - (RecommendedColorButton2, RecommendedColorSwatch2), - (RecommendedColorButton3, RecommendedColorSwatch3), - (RecommendedColorButton4, RecommendedColorSwatch4), - (RecommendedColorButton5, RecommendedColorSwatch5), - (RecommendedColorButton6, RecommendedColorSwatch6) - ]; - } - - private IReadOnlyList<(Button Button, Border Swatch)> GetMonetColorTargets() - { - return - [ - (MonetColorButton1, MonetColorSwatch1), - (MonetColorButton2, MonetColorSwatch2), - (MonetColorButton3, MonetColorSwatch3), - (MonetColorButton4, MonetColorSwatch4), - (MonetColorButton5, MonetColorSwatch5), - (MonetColorButton6, MonetColorSwatch6) - ]; - } - - private void EnsureSelectedThemeColor() - { - if (ContainsColor(_recommendedColors, _selectedThemeColor) || - ContainsColor(_monetColors, _selectedThemeColor)) - { - return; - } - - if (_recommendedColors.Count > 0) - { - _selectedThemeColor = _recommendedColors[0]; - return; - } - - if (_monetColors.Count > 0) - { - _selectedThemeColor = _monetColors[0]; - } - } - - private void ApplyThemeColorFromButton(Button? button, string sourceLabel) - { - if (!TryGetButtonColor(button, out var color)) - { - return; - } - - _selectedThemeColor = color; - UpdateThemeColorSelectionState(); - ThemeColorStatusTextBlock.Text = $"{sourceLabel} color applied: {_selectedThemeColor}."; - UpdateAdaptiveTextSystem(); - } - - private void UpdateThemeColorSelectionState() - { - UpdateColorSelectionVisuals(GetRecommendedColorTargets()); - UpdateColorSelectionVisuals(GetMonetColorTargets()); - } - - private void UpdateColorSelectionVisuals(IReadOnlyList<(Button Button, Border Swatch)> targets) - { - foreach (var (button, swatch) in targets) - { - var isSelected = TryGetButtonColor(button, out var color) && AreSameColor(color, _selectedThemeColor); - swatch.BorderBrush = isSelected - ? new SolidColorBrush(Color.Parse("#FFFFFFFF")) - : new SolidColorBrush(Color.Parse("#A0FFFFFF")); - swatch.BorderThickness = new Thickness(isSelected ? 2 : 1); - } - } - - private static bool TryGetButtonColor(Button? button, out Color color) - { - color = default; - if (button?.Tag is not string colorText || string.IsNullOrWhiteSpace(colorText)) - { - return false; - } - - try - { - color = Color.Parse(colorText); - return true; - } - catch - { - return false; - } - } - - - private static bool ContainsColor(IReadOnlyList colors, Color target) - { - for (var i = 0; i < colors.Count; i++) - { - if (AreSameColor(colors[i], target)) - { - return true; - } - } - - return false; - } - - private static bool AreSameColor(Color left, Color right) - { - return left.R == right.R && left.G == right.G && left.B == right.B; - } - - - private static double CalculateBrushLuminance(IBrush? brush) - { - if (brush is ISolidColorBrush solidBrush) - { - return CalculateRelativeLuminance(solidBrush.Color); - } - - return CalculateRelativeLuminance(Color.Parse("#FF020617")); - } - - private static double CalculateBitmapAverageLuminance(Bitmap bitmap) - { - try - { - var sampleWidth = Math.Clamp(bitmap.PixelSize.Width, 1, 48); - var sampleHeight = Math.Clamp(bitmap.PixelSize.Height, 1, 48); - - using var scaledBitmap = bitmap.CreateScaledBitmap( - new PixelSize(sampleWidth, sampleHeight), - BitmapInterpolationMode.MediumQuality); - using var writeable = new WriteableBitmap( - scaledBitmap.PixelSize, - new Vector(96, 96), - PixelFormat.Bgra8888, - AlphaFormat.Premul); - using var framebuffer = writeable.Lock(); - - scaledBitmap.CopyPixels(framebuffer, AlphaFormat.Premul); - - var rowBytes = framebuffer.RowBytes; - var byteCount = rowBytes * framebuffer.Size.Height; - if (byteCount <= 0 || framebuffer.Address == IntPtr.Zero) - { - return CalculateRelativeLuminance(Color.Parse("#FF020617")); - } - - var pixelBuffer = new byte[byteCount]; - Marshal.Copy(framebuffer.Address, pixelBuffer, 0, byteCount); - - double luminanceSum = 0; - var pixelCount = 0; - for (var y = 0; y < framebuffer.Size.Height; y++) - { - var rowOffset = y * rowBytes; - for (var x = 0; x < framebuffer.Size.Width; x++) - { - var index = rowOffset + (x * 4); - var alpha = pixelBuffer[index + 3] / 255d; - if (alpha <= 0.01) - { - continue; - } - - var blue = (pixelBuffer[index] / 255d) / alpha; - var green = (pixelBuffer[index + 1] / 255d) / alpha; - var red = (pixelBuffer[index + 2] / 255d) / alpha; - - red = Math.Clamp(red, 0, 1); - green = Math.Clamp(green, 0, 1); - blue = Math.Clamp(blue, 0, 1); - - luminanceSum += CalculateRelativeLuminance(red, green, blue); - pixelCount++; - } - } - - return pixelCount > 0 - ? luminanceSum / pixelCount - : CalculateRelativeLuminance(Color.Parse("#FF020617")); - } - catch - { - return CalculateRelativeLuminance(Color.Parse("#FF020617")); - } - } - - private static double CalculateRelativeLuminance(Color color) - { - return CalculateRelativeLuminance(color.R / 255d, color.G / 255d, color.B / 255d); - } - - private static double CalculateRelativeLuminance(double red, double green, double blue) - { - var linearRed = ToLinearRgb(red); - var linearGreen = ToLinearRgb(green); - var linearBlue = ToLinearRgb(blue); - return (0.2126 * linearRed) + (0.7152 * linearGreen) + (0.0722 * linearBlue); - } - - private static double ToLinearRgb(double value) - { - return value <= 0.04045 - ? value / 12.92 - : Math.Pow((value + 0.055) / 1.055, 2.4); - } - - private void OpenSettingsPage() - { - if (_isSettingsOpen) - { - return; - } - - _isSettingsOpen = true; - UpdateAdaptiveTextSystem(); - SettingsPage.IsVisible = true; - SettingsPage.Opacity = 0; - if (_settingsContentPanelTransform is not null) - { - _settingsContentPanelTransform.Y = 30; - } - - DesktopPage.IsHitTestVisible = false; - UpdateWallpaperPreviewLayout(); - - Dispatcher.UIThread.Post(() => - { - if (!_isSettingsOpen) - { - return; - } - - SettingsPage.Opacity = 1; - if (_settingsContentPanelTransform is not null) - { - _settingsContentPanelTransform.Y = 0; - } - }, DispatcherPriority.Background); - } - - private void CloseSettingsPage() - { - if (!_isSettingsOpen) - { - return; - } - - _isSettingsOpen = false; - UpdateAdaptiveTextSystem(); - - DesktopPage.IsHitTestVisible = true; - - SettingsPage.Opacity = 0; - if (_settingsContentPanelTransform is not null) - { - _settingsContentPanelTransform.Y = 30; - } - - DispatcherTimer.RunOnce(() => - { - if (_isSettingsOpen) - { - return; - } - - SettingsPage.IsVisible = false; - }, TimeSpan.FromMilliseconds(SettingsTransitionDurationMs)); - } - private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property != WindowStateProperty) @@ -1008,3 +450,4 @@ public partial class MainWindow : Window }); } } + diff --git a/docs/VISUAL_SPEC.md b/docs/VISUAL_SPEC.md new file mode 100644 index 0000000..c086344 --- /dev/null +++ b/docs/VISUAL_SPEC.md @@ -0,0 +1,126 @@ +# LanMontainDesktop 视觉规范(主题色 + 毛玻璃) + +## 1. 主题色应用规范 + +### 1.1 颜色角色定义 + +- `Primary`(主色):品牌主导色,用于主要操作、关键状态提示。 +- `Secondary`(辅助色):主色的低权重变体,用于次级强调、辅助信息。 +- `Accent`(强调色):可被用户替换的动态主题色,用于选中态、激活态、聚焦态。 +- `OnAccent`:放在强调色背景上的文本/图标颜色。 +- `SurfaceBase` / `SurfaceRaised` / `SurfaceOverlay`:基础背景、抬升层、遮罩层。 +- `TextPrimary` / `TextSecondary` / `TextMuted` / `TextAccent`:文字语义层级。 + +### 1.2 UI 元素映射规则 + +- 主按钮、主导航选中态:`Accent` + `OnAccent` +- 次级按钮/输入控件:`AdaptiveButtonBackgroundBrush` + `TextPrimary` +- 页头标题:`TextPrimary` +- 说明/辅助文本:`TextSecondary` / `TextMuted` +- 设置页导航激活项:`AdaptiveNavItemSelectedBackgroundBrush` + `AdaptiveNavSelectedTextBrush` + +### 1.3 统一资源键(单一真相源) + +- 主题核心: + - `AdaptivePrimaryBrush` + - `AdaptiveSecondaryBrush` + - `AdaptiveAccentBrush` + - `AdaptiveOnAccentBrush` +- 文本: + - `AdaptiveTextPrimaryBrush` + - `AdaptiveTextSecondaryBrush` + - `AdaptiveTextMutedBrush` + - `AdaptiveTextAccentBrush` +- 表面: + - `AdaptiveSurfaceBaseBrush` + - `AdaptiveSurfaceRaisedBrush` + - `AdaptiveSurfaceOverlayBrush` + +## 2. 毛玻璃(Glassmorphism)统一实现方案 + +### 2.1 分层标准 + +- `glass-overlay`:最高层遮罩(设置页背板) +- `glass-strong`:主内容容器(设置页主体) +- `glass-panel`:子功能区、组件容器(网格卡片、按钮容器) + +### 2.2 参数标准(模拟毛玻璃,跨平台稳定) + +- 描边:统一去除(`BorderThickness = 0`) +- 模糊半径资源(供样式/扩展复用): + - `AdaptiveGlassPanelBlurRadius`(日 18 / 夜 22) + - `AdaptiveGlassStrongBlurRadius`(日 24 / 夜 28) +- 透明度资源: + - `AdaptiveGlassPanelOpacity`(日 0.88 / 夜 0.92) + - `AdaptiveGlassStrongOpacity`(日 0.92 / 夜 0.95) +- 背景色:由 `GlassEffectService` 基于主题色动态混合,统一下发到: + - `AdaptiveGlassPanelBackgroundBrush` + - `AdaptiveGlassStrongBackgroundBrush` + - `AdaptiveGlassOverlayBackgroundBrush` + +## 3. 视觉一致性策略 + +- 全局样式入口:`Styles/GlassModule.axaml` +- 全局主题入口:`ThemeColorSystemService` + `GlassEffectService` +- 页面侧仅使用语义资源键和 `glass-*` 类,不写硬编码颜色 +- `MainWindow` 只负责编排:切换模式、选择主题色、触发资源重算 + +## 4. 可访问性(WCAG) + +### 4.1 对比度目标 + +- 正文文本:`>= 4.5:1` +- 大号文本 / 强调文本:`>= 3.0:1` + +### 4.2 实现方式 + +- `Theme/ColorMath.cs` 提供: + - 相对亮度计算 + - 对比度计算 + - `EnsureContrast(...)` 自动修正文本前景色 +- `ThemeColorSystemService` 在生成 `TextPrimary/TextSecondary/TextMuted/NavText` 时强制走对比度校正 + +## 5. 跨尺寸与分辨率一致性 + +- 启用像素对齐:`UseLayoutRounding="True"` + `SnapsToDevicePixels="True"` +- 桌面网格布局通过统一计算函数输出 `row/col/cell`,主视图与预览共用算法 +- 预览区域按窗口实际宽高比缩放,保持 Win11 风格比例一致性 +- 关键尺寸自适应(字体、内边距、圆角)随 `cellSize` 动态计算 + +## 6. 实现代码示例 + +### 6.1 主题系统应用(C#) + +```csharp +var context = new ThemeColorContext( + selectedAccent, + isLightBackground, + isLightNavBackground, + isNightMode); + +ThemeColorSystemService.ApplyThemeResources(Resources, context); +GlassEffectService.ApplyGlassResources(Resources, context); +``` + +### 6.2 页面层使用语义资源(AXAML) + +```xml + + + + + +