This commit is contained in:
lincube
2026-02-28 03:00:25 +08:00
parent 4ded1c1f20
commit b224f07e69
17 changed files with 2136 additions and 870 deletions

3
.arts/settings.json Normal file
View File

@@ -0,0 +1,3 @@
{
"diffEditor.renderSideBySide": false
}

View File

@@ -1,6 +1,7 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sty="using:FluentAvalonia.Styling"
xmlns:fi="using:FluentIcons.Avalonia"
x:Class="LanMontainDesktop.App"
xmlns:local="using:LanMontainDesktop"
RequestedThemeVariant="Default">
@@ -12,5 +13,32 @@
<Application.Styles>
<sty:FluentAvaloniaTheme />
<StyleInclude Source="avares://LanMontainDesktop/Styles/GlassModule.axaml" />
<Style Selector="fi|SymbolIcon">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="FontSize" Value="16" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
</Style>
<Style Selector="fi|FluentIcon">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="FontSize" Value="16" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
</Style>
<Style Selector="fi|SymbolIcon.icon-s, fi|FluentIcon.icon-s">
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="fi|SymbolIcon.icon-m, fi|FluentIcon.icon-m">
<Setter Property="FontSize" Value="16" />
</Style>
<Style Selector="fi|SymbolIcon.icon-l, fi|FluentIcon.icon-l">
<Setter Property="FontSize" Value="20" />
</Style>
</Application.Styles>
</Application>

View File

@@ -25,5 +25,8 @@
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="FluentAvaloniaUI" Version="2.5.0" />
<PackageReference Include="FluentIcons.Avalonia" Version="2.0.319" />
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" />
</ItemGroup>
</Project>

View File

@@ -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<string> TopStatusComponentIds { get; set; } = [];
public List<string> PinnedTaskbarActions { get; set; } =
[
TaskbarActionId.MinimizeToWindows.ToString(),
TaskbarActionId.OpenSettings.ToString()
];
public bool EnableDynamicTaskbarActions { get; set; } = false;
public string TaskbarLayoutMode { get; set; } = "BottomFullRowMacStyle";
}

View File

@@ -0,0 +1,8 @@
namespace LanMontainDesktop.Models;
public enum TaskbarActionId
{
MinimizeToWindows,
OpenSettings
}

View File

@@ -0,0 +1,9 @@
namespace LanMontainDesktop.Models;
public sealed record TaskbarActionItem(
TaskbarActionId Id,
string Title,
string IconKey,
bool IsVisible,
string CommandKey);

View File

@@ -0,0 +1,10 @@
namespace LanMontainDesktop.Models;
public enum TaskbarContext
{
Desktop,
SettingsWallpaper,
SettingsGrid,
SettingsColor,
SettingsStatusBar
}

View File

@@ -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<AppSettingsSnapshot>(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.
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -2,25 +2,38 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:FluentAvalonia.UI.Controls">
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
<Style Selector="Button">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.15" />
<DoubleTransition Property="Opacity" Duration="0:0:0.15" />
</Transitions>
</Setter>
</Style>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonHoverBackgroundBrush}" />
<Setter Property="RenderTransform" Value="scale(1.02)" />
</Style>
<Style Selector="Button:pressed">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonPressedBackgroundBrush}" />
<Setter Property="RenderTransform" Value="scale(0.98)" />
</Style>
<Style Selector="ComboBox">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
@@ -30,8 +43,8 @@
<Style Selector="ui|NumberBox">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
@@ -39,16 +52,41 @@
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
<Style Selector="Button.swatch-button">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Opacity" Value="0.88" />
</Style>
<Style Selector="Button.swatch-button.swatch-selected">
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
<Setter Property="Opacity" Value="1" />
<Setter Property="RenderTransform" Value="scale(1.05)" />
</Style>
<Style Selector="Border.glass-panel">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassPanelBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassPanelOpacity}" />
<Setter Property="BoxShadow" Value="0 4 20 #1A000000, 0 8 40 #0D000000" />
</Style>
<Style Selector="Border.glass-strong">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassStrongBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="16" />
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
<Setter Property="BoxShadow" Value="0 8 30 #26000000, 0 16 60 #14000000" />
</Style>
<Style Selector="Border.glass-overlay">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassOverlayBackgroundBrush}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassOverlayOpacity}" />
</Style>
</Styles>

View File

@@ -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);

View File

@@ -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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,10 @@
<Window xmlns="https://github.com/avaloniaui"
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMontainDesktop.ViewModels"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:comp="using:LanMontainDesktop.Views.Components"
xmlns:vlc="clr-namespace:LibVLCSharp.Avalonia;assembly=LibVLCSharp.Avalonia"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
@@ -14,8 +16,9 @@
WindowState="FullScreen"
SystemDecorations="None"
CanResize="False"
UseLayoutRounding="True"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Background="#FF020617"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
Title="LanMontainDesktop">
<Design.DataContext>
@@ -23,10 +26,17 @@
</Design.DataContext>
<Window.Resources>
<SolidColorBrush x:Key="AdaptivePrimaryBrush" Color="#FF1D4ED8" />
<SolidColorBrush x:Key="AdaptiveSecondaryBrush" Color="#FF1E40AF" />
<SolidColorBrush x:Key="AdaptiveTextPrimaryBrush" Color="#FFF8FAFC" />
<SolidColorBrush x:Key="AdaptiveTextSecondaryBrush" Color="#FFE2E8F0" />
<SolidColorBrush x:Key="AdaptiveTextMutedBrush" Color="#FF94A3B8" />
<SolidColorBrush x:Key="AdaptiveTextAccentBrush" Color="#FF93C5FD" />
<SolidColorBrush x:Key="AdaptiveAccentBrush" Color="#FF3B82F6" />
<SolidColorBrush x:Key="AdaptiveOnAccentBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="AdaptiveSurfaceBaseBrush" Color="#FF020617" />
<SolidColorBrush x:Key="AdaptiveSurfaceRaisedBrush" Color="#FF1E293B" />
<SolidColorBrush x:Key="AdaptiveSurfaceOverlayBrush" Color="#CC0F172A" />
<SolidColorBrush x:Key="AdaptiveButtonBackgroundBrush" Color="#66334155" />
<SolidColorBrush x:Key="AdaptiveButtonBorderBrush" Color="#80E2E8F0" />
<SolidColorBrush x:Key="AdaptiveButtonHoverBackgroundBrush" Color="#88475A74" />
@@ -35,17 +45,22 @@
<SolidColorBrush x:Key="AdaptiveGlassPanelBorderBrush" Color="#70475569" />
<SolidColorBrush x:Key="AdaptiveGlassStrongBackgroundBrush" Color="#A01E293B" />
<SolidColorBrush x:Key="AdaptiveGlassStrongBorderBrush" Color="#80475569" />
<SolidColorBrush x:Key="AdaptiveGlassOverlayBackgroundBrush" Color="#9A0F172A" />
<SolidColorBrush x:Key="AdaptiveNavTextBrush" Color="#FFF8FAFC" />
<SolidColorBrush x:Key="AdaptiveNavSelectedTextBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="AdaptiveNavSelectionIndicatorBrush" Color="#FF93C5FD" />
<SolidColorBrush x:Key="AdaptiveNavItemBackgroundBrush" Color="#220F172A" />
<SolidColorBrush x:Key="AdaptiveNavItemHoverBackgroundBrush" Color="#40334155" />
<SolidColorBrush x:Key="AdaptiveNavItemSelectedBackgroundBrush" Color="#CC1D4ED8" />
<SolidColorBrush x:Key="AdaptiveToggleOnBrush" Color="#FF3B82F6" />
<SolidColorBrush x:Key="AdaptiveToggleOffBrush" Color="#66475569" />
<SolidColorBrush x:Key="AdaptiveToggleBorderBrush" Color="#80E2E8F0" />
<x:Double x:Key="AdaptiveGlassPanelBlurRadius">22</x:Double>
<x:Double x:Key="AdaptiveGlassStrongBlurRadius">28</x:Double>
<x:Double x:Key="AdaptiveGlassPanelOpacity">0.92</x:Double>
<x:Double x:Key="AdaptiveGlassStrongOpacity">0.95</x:Double>
</Window.Resources>
<Window.Styles>
<StyleInclude Source="avares://LanMontainDesktop/Styles/GlassModule.axaml" />
</Window.Styles>
<Grid>
<Grid x:Name="DesktopPage">
<Grid.Transitions>
@@ -56,24 +71,60 @@
<Border x:Name="DesktopHost"
ClipToBounds="True"
Background="#FF020617">
Background="Transparent">
<Grid>
<Border x:Name="DesktopWallpaperLayer"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}" />
<vlc:VideoView x:Name="DesktopVideoWallpaperView"
IsVisible="False"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<Grid x:Name="DesktopGrid"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ShowGridLines="False">
<Border x:Name="TopStatusBarHost"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="1"
Background="Transparent"
BorderThickness="0"
Padding="4">
<StackPanel x:Name="TopStatusComponentsPanel"
Orientation="Horizontal"
Spacing="6">
<comp:ClockWidget x:Name="ClockWidget"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="4" />
IsVisible="False"
Margin="0" />
</StackPanel>
</Border>
<Border x:Name="BackToWindowsContainer"
Classes="glass-panel"
<Border x:Name="BottomTaskbarContainer"
Classes="glass-strong"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="4"
Grid.ColumnSpan="1"
Margin="4"
CornerRadius="10">
CornerRadius="18"
Padding="6">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<Border x:Name="TaskbarFixedActionsHost"
Grid.Column="0"
Background="Transparent"
BorderThickness="0">
<Grid ColumnDefinitions="Auto,Auto"
ColumnSpacing="8">
<Border x:Name="BackToWindowsContainer"
Grid.Column="0"
Background="Transparent"
CornerRadius="12">
<Button x:Name="BackToWindowsButton"
Padding="8"
HorizontalAlignment="Stretch"
@@ -82,20 +133,55 @@
BorderThickness="0"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Click="OnMinimizeClick"
Content="&#22238;&#21040;Windows" />
ToolTip.Tip="&#22238;&#21040;Windows">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<fi:SymbolIcon Classes="icon-m"
Symbol="Window"
IconVariant="Regular"
/>
<TextBlock Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="&#22238;&#21040;Windows" />
</StackPanel>
</Button>
</Border>
<Border x:Name="OpenSettingsContainer"
Grid.Column="1"
Background="Transparent"
CornerRadius="12">
<Button x:Name="OpenSettingsButton"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="4"
Padding="16,8"
Padding="6"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
BorderThickness="0"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Click="OnOpenSettingsClick"
Content="&#35774;&#32622;" />
ToolTip.Tip="&#35774;&#32622;">
<fi:SymbolIcon Classes="icon-l"
Symbol="Settings"
IconVariant="Regular"
/>
</Button>
</Border>
</Grid>
</Border>
<Border x:Name="TaskbarDynamicActionsHost"
Grid.Column="1"
Background="Transparent"
BorderThickness="0"
IsVisible="False">
<StackPanel x:Name="TaskbarDynamicActionsPanel"
Orientation="Horizontal"
Spacing="8" />
</Border>
</Grid>
</Border>
</Grid>
</Grid>
</Border>
</Grid>
@@ -110,8 +196,7 @@
</Grid.Transitions>
<Border x:Name="SettingsBackdropOverlay"
Classes="glass-strong"
BorderThickness="0" />
Classes="glass-overlay" />
<Border x:Name="SettingsContentPanel"
Classes="glass-strong"
@@ -156,13 +241,17 @@
CornerRadius="10"
Padding="10">
<Border.Styles>
<Style Selector="ListBox#SettingsNavListBox">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="ListBox#SettingsNavListBox ListBoxItem">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveNavTextBrush}" />
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemBackgroundBrush}" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="10,8" />
<Setter Property="Margin" Value="0,2" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="ListBox#SettingsNavListBox ListBoxItem:pointerover">
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemHoverBackgroundBrush}" />
@@ -170,7 +259,6 @@
<Style Selector="ListBox#SettingsNavListBox ListBoxItem:selected">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveNavSelectedTextBrush}" />
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
</Style>
</Border.Styles>
@@ -184,6 +272,7 @@
<ListBoxItem Content="&#22721;&#32440;" />
<ListBoxItem Content="&#32593;&#26684;" />
<ListBoxItem Content="&#39068;&#33394;" />
<ListBoxItem Content="&#29366;&#24577;&#26639;" />
</ListBox>
</StackPanel>
</Border>
@@ -212,25 +301,38 @@
Width="360"
Height="220"
CornerRadius="14"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
Background="#22000000">
<Border x:Name="WallpaperPreviewViewport"
ClipToBounds="True"
CornerRadius="13"
Background="#30111827">
<Grid>
<vlc:VideoView x:Name="WallpaperPreviewVideoView"
IsVisible="False"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<Grid x:Name="WallpaperPreviewGrid"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ShowGridLines="False">
<Border x:Name="WallpaperPreviewClockContainer"
<Border x:Name="WallpaperPreviewTopStatusBarHost"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
Grid.ColumnSpan="1"
Background="Transparent"
BorderThickness="0"
Padding="2">
<StackPanel x:Name="WallpaperPreviewTopStatusComponentsPanel"
Orientation="Horizontal"
Spacing="3">
<Border x:Name="WallpaperPreviewClockContainer"
CornerRadius="0"
BorderThickness="0"
Background="Transparent"
Margin="3">
Margin="0"
IsVisible="False">
<TextBlock x:Name="WallpaperPreviewClockTextBlock"
HorizontalAlignment="Center"
VerticalAlignment="Center"
@@ -238,36 +340,72 @@
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="12:34" />
</Border>
</StackPanel>
</Border>
<Border x:Name="WallpaperPreviewBackButtonContainer"
Classes="glass-panel"
<Border x:Name="WallpaperPreviewBottomTaskbarContainer"
Classes="glass-strong"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="4"
Grid.ColumnSpan="1"
Margin="3"
CornerRadius="7">
CornerRadius="8"
Padding="2">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="3">
<Border x:Name="WallpaperPreviewTaskbarFixedActionsHost"
Grid.Column="0"
Background="Transparent"
BorderThickness="0">
<Grid ColumnDefinitions="Auto,Auto"
ColumnSpacing="3">
<Border x:Name="WallpaperPreviewBackButtonContainer"
Grid.Column="0"
Background="Transparent"
CornerRadius="6">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="3">
<fi:SymbolIcon Classes="icon-s"
Symbol="Window"
IconVariant="Regular"
/>
<TextBlock x:Name="WallpaperPreviewBackButtonTextBlock"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="&#22238;&#21040;Windows" />
</StackPanel>
</Border>
<Border x:Name="WallpaperPreviewSettingsButtonContainer"
Classes="glass-panel"
Grid.Row="1"
Grid.Column="4"
Grid.ColumnSpan="2"
Margin="3"
CornerRadius="7">
<TextBlock x:Name="WallpaperPreviewSettingsButtonTextBlock"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="&#35774;&#32622;" />
Grid.Column="1"
Background="Transparent"
CornerRadius="6">
<fi:SymbolIcon x:Name="WallpaperPreviewSettingsButtonIcon"
Classes="icon-s"
Symbol="Settings"
IconVariant="Regular"
/>
</Border>
</Grid>
</Border>
<Border x:Name="WallpaperPreviewTaskbarDynamicActionsHost"
Grid.Column="1"
Background="Transparent"
BorderThickness="0"
IsVisible="False">
<StackPanel x:Name="WallpaperPreviewTaskbarDynamicActionsPanel"
Orientation="Horizontal"
Spacing="3" />
</Border>
</Grid>
</Border>
</Grid>
</Grid>
</Border>
</Border>
</Border>
@@ -406,8 +544,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton2"
Width="68"
@@ -418,8 +555,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton3"
Width="68"
@@ -430,8 +566,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton4"
Width="68"
@@ -442,8 +577,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton5"
Width="68"
@@ -454,8 +588,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton6"
Width="68"
@@ -466,8 +599,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
</WrapPanel>
@@ -494,8 +626,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton2"
Width="68"
@@ -506,8 +637,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton3"
Width="68"
@@ -518,8 +648,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton4"
Width="68"
@@ -530,8 +659,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton5"
Width="68"
@@ -542,8 +670,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton6"
Width="68"
@@ -554,8 +681,7 @@
Width="26"
Height="26"
CornerRadius="6"
BorderBrush="#A0FFFFFF"
BorderThickness="1" />
BorderThickness="0" />
</Button>
</WrapPanel>
@@ -563,6 +689,33 @@
Foreground="{DynamicResource AdaptiveTextMutedBrush}"
Text="Theme color is ready." />
</StackPanel>
<StackPanel x:Name="StatusBarSettingsPanel"
IsVisible="False"
Spacing="14">
<TextBlock FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="Status Bar" />
<TextBlock Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="Choose which components appear on the top status bar." />
<ui:SettingsExpander Header="&#26102;&#38388;&#32452;&#20214;">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="12">
<ToggleSwitch x:Name="StatusBarClockToggleSwitch"
Grid.Column="0"
OnContent="On"
OffContent="Off"
Checked="OnStatusBarClockChecked"
Unchecked="OnStatusBarClockUnchecked" />
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="&#22312;&#39030;&#37096;&#29366;&#24577;&#26639;&#26174;&#31034;&#26102;&#38047;&#12290;" />
</Grid>
</ui:SettingsExpander>
</StackPanel>
</Grid>
</Border>
</Grid>
@@ -582,3 +735,6 @@
</Grid>
</Window>

View File

@@ -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<string> SupportedImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp"
};
private static readonly HashSet<string> 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<string> _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<TaskbarActionId> _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<Color> _recommendedColors = Array.Empty<Color>();
private IReadOnlyList<Color> _monetColors = Array.Empty<Color>();
private Color _selectedThemeColor = Color.Parse("#FF3B82F6");
private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle;
public MainWindow()
{
InitializeComponent();
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().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<Color> 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<Color> 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
});
}
}

126
docs/VISUAL_SPEC.md Normal file
View File

@@ -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
<Border Classes="glass-overlay" />
<Border Classes="glass-strong" CornerRadius="16">
<Border Classes="glass-panel" CornerRadius="10" Padding="14">
<TextBlock Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Button Background="{DynamicResource AdaptiveButtonBackgroundBrush}" />
</Border>
</Border>
```
### 6.3 无描边层级区分原则AXAML
```xml
<Style Selector="Border.glass-panel">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassPanelOpacity}" />
</Style>
```