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" />
+ BorderThickness="0" />
+ BorderThickness="0" />
+ BorderThickness="0" />
+ BorderThickness="0" />
+ BorderThickness="0" />
@@ -494,8 +626,7 @@
Width="26"
Height="26"
CornerRadius="6"
- BorderBrush="#A0FFFFFF"
- BorderThickness="1" />
+ BorderThickness="0" />
+ BorderThickness="0" />
+ BorderThickness="0" />
+ BorderThickness="0" />
+ BorderThickness="0" />
+ 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
+
+
+
+
+
+
+
+
+```
+
+### 6.3 无描边层级区分原则(AXAML)
+
+```xml
+
+```
+