mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.1.5
This commit is contained in:
3
.arts/settings.json
Normal file
3
.arts/settings.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"diffEditor.renderSideBySide": false
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
30
LanMontainDesktop/Models/AppSettingsSnapshot.cs
Normal file
30
LanMontainDesktop/Models/AppSettingsSnapshot.cs
Normal 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";
|
||||
}
|
||||
8
LanMontainDesktop/Models/TaskbarActionId.cs
Normal file
8
LanMontainDesktop/Models/TaskbarActionId.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace LanMontainDesktop.Models;
|
||||
|
||||
public enum TaskbarActionId
|
||||
{
|
||||
MinimizeToWindows,
|
||||
OpenSettings
|
||||
}
|
||||
|
||||
9
LanMontainDesktop/Models/TaskbarActionItem.cs
Normal file
9
LanMontainDesktop/Models/TaskbarActionItem.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace LanMontainDesktop.Models;
|
||||
|
||||
public sealed record TaskbarActionItem(
|
||||
TaskbarActionId Id,
|
||||
string Title,
|
||||
string IconKey,
|
||||
bool IsVisible,
|
||||
string CommandKey);
|
||||
|
||||
10
LanMontainDesktop/Models/TaskbarContext.cs
Normal file
10
LanMontainDesktop/Models/TaskbarContext.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace LanMontainDesktop.Models;
|
||||
|
||||
public enum TaskbarContext
|
||||
{
|
||||
Desktop,
|
||||
SettingsWallpaper,
|
||||
SettingsGrid,
|
||||
SettingsColor,
|
||||
SettingsStatusBar
|
||||
}
|
||||
62
LanMontainDesktop/Services/AppSettingsService.cs
Normal file
62
LanMontainDesktop/Services/AppSettingsService.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
61
LanMontainDesktop/Theme/ColorMath.cs
Normal file
61
LanMontainDesktop/Theme/ColorMath.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
1187
LanMontainDesktop/Views/MainWindow.Settings.cs
Normal file
1187
LanMontainDesktop/Views/MainWindow.Settings.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,46 +71,117 @@
|
||||
|
||||
<Border x:Name="DesktopHost"
|
||||
ClipToBounds="True"
|
||||
Background="#FF020617">
|
||||
<Grid x:Name="DesktopGrid"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
ShowGridLines="False">
|
||||
<comp:ClockWidget x:Name="ClockWidget"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
Margin="4" />
|
||||
|
||||
<Border x:Name="BackToWindowsContainer"
|
||||
Classes="glass-panel"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="4"
|
||||
Margin="4"
|
||||
CornerRadius="10">
|
||||
<Button x:Name="BackToWindowsButton"
|
||||
Padding="8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Click="OnMinimizeClick"
|
||||
Content="回到Windows" />
|
||||
</Border>
|
||||
|
||||
<Button x:Name="OpenSettingsButton"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="4"
|
||||
Padding="16,8"
|
||||
Background="Transparent">
|
||||
<Grid>
|
||||
<Border x:Name="DesktopWallpaperLayer"
|
||||
IsHitTestVisible="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Click="OnOpenSettingsClick"
|
||||
Content="设置" />
|
||||
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"
|
||||
IsVisible="False"
|
||||
Margin="0" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="BottomTaskbarContainer"
|
||||
Classes="glass-strong"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="1"
|
||||
Margin="4"
|
||||
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"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Click="OnMinimizeClick"
|
||||
ToolTip.Tip="回到Windows">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<fi:SymbolIcon Classes="icon-m"
|
||||
Symbol="Window"
|
||||
IconVariant="Regular"
|
||||
/>
|
||||
<TextBlock Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="回到Windows" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="OpenSettingsContainer"
|
||||
Grid.Column="1"
|
||||
Background="Transparent"
|
||||
CornerRadius="12">
|
||||
<Button x:Name="OpenSettingsButton"
|
||||
Padding="6"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Click="OnOpenSettingsClick"
|
||||
ToolTip.Tip="设置">
|
||||
<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="壁纸" />
|
||||
<ListBoxItem Content="网格" />
|
||||
<ListBoxItem Content="颜色" />
|
||||
<ListBoxItem Content="状态栏" />
|
||||
</ListBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
@@ -212,60 +301,109 @@
|
||||
Width="360"
|
||||
Height="220"
|
||||
CornerRadius="14"
|
||||
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
||||
BorderThickness="1"
|
||||
Background="#22000000">
|
||||
<Border x:Name="WallpaperPreviewViewport"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="13"
|
||||
Background="#30111827">
|
||||
<Grid x:Name="WallpaperPreviewGrid"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
ShowGridLines="False">
|
||||
<Border x:Name="WallpaperPreviewClockContainer"
|
||||
<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="WallpaperPreviewTopStatusBarHost"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
CornerRadius="0"
|
||||
BorderThickness="0"
|
||||
Grid.ColumnSpan="1"
|
||||
Background="Transparent"
|
||||
Margin="3">
|
||||
<TextBlock x:Name="WallpaperPreviewClockTextBlock"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="12:34" />
|
||||
BorderThickness="0"
|
||||
Padding="2">
|
||||
<StackPanel x:Name="WallpaperPreviewTopStatusComponentsPanel"
|
||||
Orientation="Horizontal"
|
||||
Spacing="3">
|
||||
<Border x:Name="WallpaperPreviewClockContainer"
|
||||
CornerRadius="0"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
Margin="0"
|
||||
IsVisible="False">
|
||||
<TextBlock x:Name="WallpaperPreviewClockTextBlock"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
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">
|
||||
<TextBlock x:Name="WallpaperPreviewBackButtonTextBlock"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="回到Windows" />
|
||||
</Border>
|
||||
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="回到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="设置" />
|
||||
<Border x:Name="WallpaperPreviewSettingsButtonContainer"
|
||||
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>
|
||||
@@ -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="时间组件">
|
||||
<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="在顶部状态栏显示时钟。" />
|
||||
</Grid>
|
||||
</ui:SettingsExpander>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
@@ -582,3 +735,6 @@
|
||||
</Grid>
|
||||
|
||||
</Window>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
126
docs/VISUAL_SPEC.md
Normal 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>
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user