diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs
index 3764f2a..2e4f890 100644
--- a/LanMountainDesktop/App.axaml.cs
+++ b/LanMountainDesktop/App.axaml.cs
@@ -8,6 +8,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
+using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
@@ -17,6 +18,7 @@ using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
+using LanMountainDesktop.Theme;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views;
@@ -24,6 +26,7 @@ namespace LanMountainDesktop;
public partial class App : Application
{
+ private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6");
private enum DesktopShellState
{
ForegroundDesktop = 0,
@@ -75,13 +78,18 @@ public partial class App : Application
PageId: pageTag));
}
+ public App()
+ {
+ _settingsFacade.Settings.Changed += OnSettingsChanged;
+ }
+
public override void Initialize()
{
AppLogger.Info("App", "Initializing application resources.");
ConfigureWebViewUserDataFolder();
AvaloniaWebViewBuilder.Initialize(default);
AvaloniaXamlLoader.Load(this);
- ApplyInitialThemeVariantFromSettings();
+ ApplyThemeFromSettings();
ApplyCurrentCultureFromSettings();
EnsureSettingsWindowService();
}
@@ -292,12 +300,13 @@ public partial class App : Application
_settingsFacade);
}
- private void ApplyInitialThemeVariantFromSettings()
+ private void ApplyThemeFromSettings()
{
var themeState = _settingsFacade.Theme.Get();
RequestedThemeVariant = themeState.IsNightMode
? ThemeVariant.Dark
: ThemeVariant.Light;
+ ApplyAdaptiveThemeResources(themeState);
}
private void ApplyCurrentCultureFromSettings()
@@ -424,7 +433,7 @@ public partial class App : Application
{
Dispatcher.UIThread.Post(() =>
{
- ApplyInitialThemeVariantFromSettings();
+ ApplyThemeFromSettings();
ApplyCurrentCultureFromSettings();
if (_trayIcons is not null)
{
@@ -433,6 +442,71 @@ public partial class App : Application
}, DispatcherPriority.Background);
}
+ private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
+ {
+ _ = sender;
+
+ if (e.Scope != SettingsScope.App)
+ {
+ return;
+ }
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ var changedKeys = e.ChangedKeys?.ToArray();
+ var refreshAll = changedKeys is null || changedKeys.Length == 0;
+ var themeChanged =
+ refreshAll ||
+ changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
+ changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase);
+ var languageChanged =
+ refreshAll ||
+ changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
+
+ if (themeChanged)
+ {
+ ApplyThemeFromSettings();
+ }
+
+ if (languageChanged)
+ {
+ ApplyCurrentCultureFromSettings();
+ if (_trayIcons is not null)
+ {
+ InitializeTrayIcon();
+ }
+ }
+ }, DispatcherPriority.Background);
+ }
+
+ private void ApplyAdaptiveThemeResources(ThemeAppearanceSettingsState themeState)
+ {
+ var accentColor = TryParseThemeColor(themeState.ThemeColor);
+ var context = new ThemeColorContext(
+ accentColor,
+ IsLightBackground: !themeState.IsNightMode,
+ IsLightNavBackground: !themeState.IsNightMode,
+ IsNightMode: themeState.IsNightMode);
+ ThemeColorSystemService.ApplyThemeResources(Resources, context);
+ GlassEffectService.ApplyGlassResources(Resources, context);
+ }
+
+ private static Color TryParseThemeColor(string? colorText)
+ {
+ if (!string.IsNullOrWhiteSpace(colorText))
+ {
+ try
+ {
+ return Color.Parse(colorText);
+ }
+ catch
+ {
+ }
+ }
+
+ return DefaultAccentColor;
+ }
+
private void RegisterUiUnhandledExceptionGuard()
{
if (_uiUnhandledExceptionHooked)
@@ -479,6 +553,7 @@ public partial class App : Application
_exitCleanupCompleted = true;
AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
+ _settingsFacade.Settings.Changed -= OnSettingsChanged;
try
{
diff --git a/LanMountainDesktop/Controls/IconText.axaml b/LanMountainDesktop/Controls/IconText.axaml
new file mode 100644
index 0000000..da69f8e
--- /dev/null
+++ b/LanMountainDesktop/Controls/IconText.axaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Controls/IconText.axaml.cs b/LanMountainDesktop/Controls/IconText.axaml.cs
new file mode 100644
index 0000000..92b95b5
--- /dev/null
+++ b/LanMountainDesktop/Controls/IconText.axaml.cs
@@ -0,0 +1,52 @@
+using Avalonia;
+using Avalonia.Controls;
+using FluentIcons.Avalonia;
+using FluentIcons.Common;
+
+namespace LanMountainDesktop.Controls;
+
+public partial class IconText : UserControl
+{
+ public static readonly StyledProperty IconProperty =
+ AvaloniaProperty.Register(nameof(Icon), Icon.Info);
+
+ public static readonly StyledProperty TextProperty =
+ AvaloniaProperty.Register(nameof(Text), string.Empty);
+
+ public IconText()
+ {
+ InitializeComponent();
+ }
+
+ public Icon Icon
+ {
+ get => GetValue(IconProperty);
+ set => SetValue(IconProperty, value);
+ }
+
+ public string Text
+ {
+ get => GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == IconProperty)
+ {
+ if (IconElement is not null)
+ {
+ IconElement.Icon = change.GetNewValue();
+ }
+ }
+ else if (change.Property == TextProperty)
+ {
+ if (TextElement is not null)
+ {
+ TextElement.Text = change.GetNewValue();
+ }
+ }
+ }
+}
diff --git a/LanMountainDesktop/Controls/SettingsOptionCard.axaml.cs b/LanMountainDesktop/Controls/SettingsOptionCard.axaml.cs
index 984cfe7..60b02ea 100644
--- a/LanMountainDesktop/Controls/SettingsOptionCard.axaml.cs
+++ b/LanMountainDesktop/Controls/SettingsOptionCard.axaml.cs
@@ -102,6 +102,7 @@ public partial class SettingsOptionCard : UserControl
return iconKey?.Trim() switch
{
"DesignIdeas" => Symbol.Color,
+ "Image" => Symbol.Image,
"GridDots" => Symbol.GridDots,
"PuzzlePiece" => Symbol.PuzzlePiece,
"Info" => Symbol.Info,
diff --git a/LanMountainDesktop/Controls/SettingsSectionCard.axaml.cs b/LanMountainDesktop/Controls/SettingsSectionCard.axaml.cs
index 813f0b8..baee282 100644
--- a/LanMountainDesktop/Controls/SettingsSectionCard.axaml.cs
+++ b/LanMountainDesktop/Controls/SettingsSectionCard.axaml.cs
@@ -87,6 +87,7 @@ public partial class SettingsSectionCard : UserControl
return iconKey?.Trim() switch
{
"DesignIdeas" => Symbol.Color,
+ "Image" => Symbol.Image,
"GridDots" => Symbol.GridDots,
"PuzzlePiece" => Symbol.PuzzlePiece,
"Info" => Symbol.Info,
diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json
index 6e30881..f366cc0 100644
--- a/LanMountainDesktop/Localization/en-US.json
+++ b/LanMountainDesktop/Localization/en-US.json
@@ -232,7 +232,7 @@
"settings.general.preview_date_label": "Date",
"settings.general.render_mode_restart_message": "Rendering mode changes require restarting the app.",
"settings.appearance.title": "Appearance",
- "settings.appearance.description": "Adjust theme, wallpaper, and status bar presentation.",
+ "settings.appearance.description": "Adjust theme and status bar presentation.",
"settings.appearance.theme_header": "Theme",
"settings.color.enable_night_mode_toggle": "Enable night mode",
"settings.color.use_system_chrome_toggle": "Use system window chrome",
@@ -802,4 +802,3 @@
"single_instance.notice.button": "OK"
}
-
diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json
index 5b54ad7..f037590 100644
--- a/LanMountainDesktop/Localization/zh-CN.json
+++ b/LanMountainDesktop/Localization/zh-CN.json
@@ -232,7 +232,7 @@
"settings.general.preview_date_label": "日期",
"settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。",
"settings.appearance.title": "外观",
- "settings.appearance.description": "切换主题、壁纸和状态栏展示。",
+ "settings.appearance.description": "切换主题与状态栏展示。",
"settings.appearance.theme_header": "主题",
"settings.color.enable_night_mode_toggle": "启用夜间模式",
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
@@ -802,4 +802,3 @@
"single_instance.notice.button": "确定"
}
-
diff --git a/LanMountainDesktop/Services/Settings/SettingsWindowService.cs b/LanMountainDesktop/Services/Settings/SettingsWindowService.cs
index decaef0..8185a82 100644
--- a/LanMountainDesktop/Services/Settings/SettingsWindowService.cs
+++ b/LanMountainDesktop/Services/Settings/SettingsWindowService.cs
@@ -2,11 +2,13 @@ using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
+using LanMountainDesktop.Theme;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views;
@@ -50,6 +52,7 @@ public interface ISettingsWindowService
internal sealed class SettingsWindowService : ISettingsWindowService
{
+ private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6");
private readonly ISettingsPageRegistry _pageRegistry;
private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
private readonly ISettingsFacadeService _settingsFacade;
@@ -85,7 +88,7 @@ internal sealed class SettingsWindowService : ISettingsWindowService
_window ??= CreateWindow();
var themeState = _settingsFacade.Theme.Get();
_window.ApplyChromeMode(themeState.UseSystemChrome);
- ApplyTheme(_window, themeState.IsNightMode);
+ ApplyTheme(_window, themeState);
_window.ReloadPages(request.PageId);
PositionWindow(_window, request);
@@ -140,7 +143,7 @@ internal sealed class SettingsWindowService : ISettingsWindowService
_pageRegistry,
_hostApplicationLifecycle,
useSystemChrome);
- ApplyTheme(window, themeState.IsNightMode);
+ ApplyTheme(window, themeState);
window.ShowInTaskbar = false;
window.Closed += (_, _) =>
{
@@ -277,6 +280,7 @@ internal sealed class SettingsWindowService : ISettingsWindowService
var themeChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
+ changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase);
if (languageChanged)
@@ -292,15 +296,40 @@ internal sealed class SettingsWindowService : ISettingsWindowService
{
var themeState = _settingsFacade.Theme.Get();
_window.ApplyChromeMode(themeState.UseSystemChrome);
- ApplyTheme(_window, themeState.IsNightMode);
+ ApplyTheme(_window, themeState);
}
}, DispatcherPriority.Background);
}
- private static void ApplyTheme(SettingsWindow window, bool isNightMode)
+ private static void ApplyTheme(SettingsWindow window, ThemeAppearanceSettingsState themeState)
{
- window.RequestedThemeVariant = isNightMode
+ window.RequestedThemeVariant = themeState.IsNightMode
? ThemeVariant.Dark
: ThemeVariant.Light;
+
+ var accentColor = TryParseThemeColor(themeState.ThemeColor);
+ var context = new ThemeColorContext(
+ accentColor,
+ IsLightBackground: !themeState.IsNightMode,
+ IsLightNavBackground: !themeState.IsNightMode,
+ IsNightMode: themeState.IsNightMode);
+ ThemeColorSystemService.ApplyThemeResources(window.Resources, context);
+ GlassEffectService.ApplyGlassResources(window.Resources, context);
+ }
+
+ private static Color TryParseThemeColor(string? colorText)
+ {
+ if (!string.IsNullOrWhiteSpace(colorText))
+ {
+ try
+ {
+ return Color.Parse(colorText);
+ }
+ catch
+ {
+ }
+ }
+
+ return DefaultAccentColor;
}
}
diff --git a/LanMountainDesktop/Styles/GlassModule.axaml b/LanMountainDesktop/Styles/GlassModule.axaml
index 4e20c87..9fd498e 100644
--- a/LanMountainDesktop/Styles/GlassModule.axaml
+++ b/LanMountainDesktop/Styles/GlassModule.axaml
@@ -107,11 +107,21 @@
+
+
+
+
+
+
@@ -33,6 +32,7 @@
+
+
diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
index d1dbe9b..d80c552 100644
--- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs
+++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
@@ -41,6 +41,9 @@ public sealed partial class SettingsWindowViewModel : ViewModelBase
[ObservableProperty]
private string _currentPageTitle = string.Empty;
+ [ObservableProperty]
+ private bool _isPageTitleVisible = true;
+
[ObservableProperty]
private string? _currentPageDescription;
@@ -636,7 +639,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
private void RefreshLocalizedText()
{
PageTitle = L("settings.appearance.title", "Appearance");
- PageDescription = L("settings.appearance.description", "Theme, wallpaper, and status bar presentation.");
+ PageDescription = L("settings.appearance.description", "Theme and status bar presentation.");
ThemeHeader = L("settings.appearance.theme_header", "Theme");
NightModeLabel = L("settings.color.enable_night_mode_toggle", "Enable night mode");
UseSystemChromeLabel = L("settings.color.use_system_chrome_toggle", "Use system window chrome");
diff --git a/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs b/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs
new file mode 100644
index 0000000..c7696b4
--- /dev/null
+++ b/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using LanMountainDesktop.Services;
+using LanMountainDesktop.Services.Settings;
+
+namespace LanMountainDesktop.ViewModels;
+
+public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
+{
+ private readonly ISettingsFacadeService _settingsFacade;
+ private readonly LocalizationService _localizationService = new();
+ private readonly string _languageCode;
+ private bool _isInitializing;
+
+ public WallpaperSettingsPageViewModel(ISettingsFacadeService settingsFacade)
+ {
+ _settingsFacade = settingsFacade;
+ _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
+ WallpaperPlacements = CreateWallpaperPlacements();
+ RefreshLocalizedText();
+
+ _isInitializing = true;
+ Load();
+ _isInitializing = false;
+ }
+
+ public IReadOnlyList WallpaperPlacements { get; }
+
+ [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty]
+ private string _wallpaperPath = string.Empty;
+
+ [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty]
+ private SelectionOption _selectedWallpaperPlacement = new("Fill", "Fill");
+
+ [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty]
+ private string _wallpaperHeader = string.Empty;
+
+ [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty]
+ private string _wallpaperPathLabel = string.Empty;
+
+ [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty]
+ private string _wallpaperPlacementLabel = string.Empty;
+
+ [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty]
+ private string _wallpaperPlacementDescription = string.Empty;
+
+ [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty]
+ private string _importWallpaperButtonText = string.Empty;
+
+ [CommunityToolkit.Mvvm.ComponentModel.ObservableProperty]
+ private string _filePickerTitle = string.Empty;
+
+ public void Load()
+ {
+ var wallpaper = _settingsFacade.Wallpaper.Get();
+ WallpaperPath = wallpaper.WallpaperPath ?? string.Empty;
+ var wallpaperPlacement = string.IsNullOrWhiteSpace(wallpaper.Placement)
+ ? "Fill"
+ : wallpaper.Placement;
+ SelectedWallpaperPlacement = WallpaperPlacements.FirstOrDefault(option =>
+ string.Equals(option.Value, wallpaperPlacement, StringComparison.OrdinalIgnoreCase))
+ ?? WallpaperPlacements[0];
+ }
+
+ public async Task ImportWallpaperAsync(string sourcePath)
+ {
+ var importedPath = await _settingsFacade.WallpaperMedia.ImportAssetAsync(sourcePath);
+ if (!string.IsNullOrWhiteSpace(importedPath))
+ {
+ WallpaperPath = importedPath;
+ }
+ }
+
+ partial void OnWallpaperPathChanged(string value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ SaveWallpaper();
+ }
+
+ partial void OnSelectedWallpaperPlacementChanged(SelectionOption value)
+ {
+ if (_isInitializing || value is null)
+ {
+ return;
+ }
+
+ SaveWallpaper();
+ }
+
+ private void SaveWallpaper()
+ {
+ _settingsFacade.Wallpaper.Save(new WallpaperSettingsState(
+ string.IsNullOrWhiteSpace(WallpaperPath) ? null : WallpaperPath,
+ SelectedWallpaperPlacement.Value));
+ }
+
+ private IReadOnlyList CreateWallpaperPlacements()
+ {
+ return
+ [
+ new SelectionOption("Fill", L("settings.wallpaper.placement.fill", "Fill")),
+ new SelectionOption("Fit", L("settings.wallpaper.placement.fit", "Fit")),
+ new SelectionOption("Stretch", L("settings.wallpaper.placement.stretch", "Stretch")),
+ new SelectionOption("Center", L("settings.wallpaper.placement.center", "Center")),
+ new SelectionOption("Tile", L("settings.wallpaper.placement.tile", "Tile"))
+ ];
+ }
+
+ private void RefreshLocalizedText()
+ {
+ WallpaperHeader = L("settings.wallpaper.title", "Wallpaper");
+ WallpaperPathLabel = L("settings.wallpaper.current_label", "Current Wallpaper");
+ WallpaperPlacementLabel = L("settings.wallpaper.placement_label", "Placement");
+ WallpaperPlacementDescription = L("settings.wallpaper.placement_desc", "Adjust how the image fills the desktop.");
+ ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper");
+ FilePickerTitle = L("filepicker.title", "Select wallpaper");
+ }
+
+ private string L(string key, string fallback)
+ => _localizationService.GetString(_languageCode, key, fallback);
+}
diff --git a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
index b4db42a..d237dbe 100644
--- a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
+++ b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
@@ -13,6 +13,7 @@ using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
+using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Views;
@@ -193,6 +194,30 @@ public partial class MainWindow
return false;
}
+ private ThemeColorContext BuildAdaptiveThemeContext()
+ {
+ return new ThemeColorContext(
+ _selectedThemeColor,
+ IsLightBackground: !_isNightMode,
+ IsLightNavBackground: !_isNightMode,
+ IsNightMode: _isNightMode);
+ }
+
+ private void ApplyAdaptiveThemeResources()
+ {
+ var context = BuildAdaptiveThemeContext();
+ ThemeColorSystemService.ApplyThemeResources(Resources, context);
+ GlassEffectService.ApplyGlassResources(Resources, context);
+
+ if (Application.Current?.Resources is { } applicationResources)
+ {
+ ThemeColorSystemService.ApplyThemeResources(applicationResources, context);
+ GlassEffectService.ApplyGlassResources(applicationResources, context);
+ }
+
+ _defaultDesktopBackground = GetThemeBrush("AdaptiveSurfaceBaseBrush");
+ }
+
private void TryRestoreWallpaper(string? savedWallpaperPath)
{
_wallpaperPath = string.IsNullOrWhiteSpace(savedWallpaperPath) ? null : savedWallpaperPath;
@@ -285,6 +310,9 @@ public partial class MainWindow
Application.Current.RequestedThemeVariant = requestedThemeVariant;
}
+ ApplyAdaptiveThemeResources();
+ ApplyWallpaperBrush();
+
if (!refreshPalettes)
{
return;
diff --git a/LanMountainDesktop/Views/MainWindow.axaml b/LanMountainDesktop/Views/MainWindow.axaml
index 7817c5b..f75eed1 100644
--- a/LanMountainDesktop/Views/MainWindow.axaml
+++ b/LanMountainDesktop/Views/MainWindow.axaml
@@ -25,43 +25,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 22
- 28
- 0.92
- 0.95
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml
index 1f0490c..c7bfff2 100644
--- a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml
@@ -2,96 +2,79 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:controls="using:LanMountainDesktop.Controls"
+ xmlns:ui="using:FluentAvalonia.UI.Controls"
+ xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.AppearanceSettingsPage"
x:DataType="vm:AppearanceSettingsPageViewModel">
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
-
-
+
+
+
+
+
-
-
+
+
-
-
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs
index 64fc011..ef96593 100644
--- a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs
+++ b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs
@@ -1,9 +1,6 @@
-using Avalonia.Controls;
-using Avalonia.Platform.Storage;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
-using System.Linq;
namespace LanMountainDesktop.Views.SettingsPages;
@@ -30,25 +27,4 @@ public partial class AppearanceSettingsPage : SettingsPageBase
}
public AppearanceSettingsPageViewModel ViewModel { get; }
-
- private async void OnBrowseWallpaperClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
- {
- if (TopLevel.GetTopLevel(this)?.StorageProvider is not { } storageProvider)
- {
- return;
- }
-
- var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
- {
- Title = ViewModel.FilePickerTitle,
- AllowMultiple = false
- });
-
- var file = files.FirstOrDefault();
- var localPath = file?.TryGetLocalPath();
- if (!string.IsNullOrWhiteSpace(localPath))
- {
- await ViewModel.ImportWallpaperAsync(localPath);
- }
- }
}
diff --git a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml
index d8daa65..b090c82 100644
--- a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml
@@ -1,40 +1,56 @@
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
@@ -42,9 +58,10 @@
-
-
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml
index a3433a0..8376bd6 100644
--- a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml
@@ -3,18 +3,22 @@
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:controls="using:LanMountainDesktop.Controls"
xmlns:ui="using:FluentAvalonia.UI.Controls"
+ xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.GeneralSettingsPage"
x:DataType="vm:GeneralSettingsPageViewModel">
-
-
-
-
+
+
+
+
+
+
+
+
@@ -24,13 +28,15 @@
-
-
+
+
-
-
+
+
+
+
+
@@ -40,12 +46,14 @@
-
-
+
+
-
-
+
+
+
+
+
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml
index 7dbad77..67549be 100644
--- a/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml
@@ -1,87 +1,92 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
-
-
+
+
+
+
+
-
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+
-
+
+
+
+
-
-
+
+
+
+
+
-
-
-
+
+
-
-
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml
new file mode 100644
index 0000000..2b18df9
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml.cs
new file mode 100644
index 0000000..5b4ad3c
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml.cs
@@ -0,0 +1,54 @@
+using Avalonia.Controls;
+using Avalonia.Platform.Storage;
+using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Services.Settings;
+using LanMountainDesktop.ViewModels;
+using System.Linq;
+
+namespace LanMountainDesktop.Views.SettingsPages;
+
+[SettingsPageInfo(
+ "wallpaper",
+ "Wallpaper",
+ SettingsPageCategory.Appearance,
+ IconKey = "Image",
+ SortOrder = 15,
+ TitleLocalizationKey = "settings.wallpaper.title",
+ DescriptionLocalizationKey = "settings.wallpaper.description")]
+public partial class WallpaperSettingsPage : SettingsPageBase
+{
+ public WallpaperSettingsPage()
+ : this(new WallpaperSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
+ {
+ }
+
+ public WallpaperSettingsPage(WallpaperSettingsPageViewModel viewModel)
+ {
+ ViewModel = viewModel;
+ DataContext = ViewModel;
+ InitializeComponent();
+ }
+
+ public WallpaperSettingsPageViewModel ViewModel { get; }
+
+ private async void OnBrowseWallpaperClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ if (TopLevel.GetTopLevel(this)?.StorageProvider is not { } storageProvider)
+ {
+ return;
+ }
+
+ var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+ {
+ Title = ViewModel.FilePickerTitle,
+ AllowMultiple = false
+ });
+
+ var file = files.FirstOrDefault();
+ var localPath = file?.TryGetLocalPath();
+ if (!string.IsNullOrWhiteSpace(localPath))
+ {
+ await ViewModel.ImportWallpaperAsync(localPath);
+ }
+ }
+}
diff --git a/LanMountainDesktop/Views/SettingsWindow.axaml b/LanMountainDesktop/Views/SettingsWindow.axaml
index 5ff523d..3a67616 100644
--- a/LanMountainDesktop/Views/SettingsWindow.axaml
+++ b/LanMountainDesktop/Views/SettingsWindow.axaml
@@ -7,8 +7,8 @@
x:DataType="vm:SettingsWindowViewModel"
Width="1120"
Height="760"
- MinWidth="920"
- MinHeight="620"
+ MinWidth="560"
+ MinHeight="480"
CanResize="True"
WindowStartupLocation="Manual"
SystemDecorations="BorderOnly"
@@ -17,7 +17,29 @@
Icon="/Assets/avalonia-logo.ico"
Title="{Binding Title}">
-
+ 920
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
_cachedPages = new(StringComparer.OrdinalIgnoreCase);
private readonly bool _useSystemChrome;
+ private bool _isResponsiveLayoutRefreshPending;
public SettingsWindow()
: this(
@@ -44,6 +52,11 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
InitializeComponent();
ApplyChromeMode(useSystemChrome);
+ if (RootNavigationView is not null)
+ {
+ RootNavigationView.PropertyChanged += OnRootNavigationViewPropertyChanged;
+ }
+
Opened += OnOpened;
SizeChanged += OnWindowSizeChanged;
Closed += OnClosed;
@@ -59,6 +72,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
SyncTitleText();
UpdateChromeMetrics();
UpdatePaneToggleIcon();
+ UpdateResponsiveLayout();
+ RequestResponsiveLayoutRefresh();
}
public void ReloadPages(string? pageId)
@@ -86,6 +101,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
ViewModel.DrawerTitle = title ?? ViewModel.DrawerFallbackTitle;
ViewModel.IsDrawerOpen = true;
SyncTitleText();
+ UpdateResponsiveLayout();
+ RequestResponsiveLayoutRefresh();
}
public void CloseDrawer()
@@ -98,6 +115,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
ViewModel.IsDrawerOpen = false;
ViewModel.DrawerTitle = null;
SyncTitleText();
+ UpdateResponsiveLayout();
+ RequestResponsiveLayoutRefresh();
}
public void RequestRestart(string? reason = null)
@@ -285,6 +304,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
_ = sender;
_ = e;
UpdateChromeMetrics();
+ UpdateResponsiveLayout();
+ RequestResponsiveLayoutRefresh();
}
private void OnWindowSizeChanged(object? sender, SizeChangedEventArgs e)
@@ -292,12 +313,139 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
_ = sender;
_ = e;
UpdateChromeMetrics();
+ UpdateResponsiveLayout();
+ RequestResponsiveLayoutRefresh();
+ }
+
+ private bool TryApplyResponsiveLayout()
+ {
+ if (SettingsContentGrid is null || DrawerBorder is null)
+ {
+ return false;
+ }
+
+ var width = Bounds.Width > 1 ? Bounds.Width : Math.Max(Width, MinWidth);
+ var renderScale = RenderScaling > 0 ? RenderScaling : 1d;
+ var titleScale = WindowTitleTextBlock?.FontSize > 0
+ ? WindowTitleTextBlock.FontSize / 12d
+ : 1d;
+ var pageTitleScale = PageTitleTextBlock?.FontSize > 0
+ ? PageTitleTextBlock.FontSize / 28d
+ : 1d;
+ var typographyScale = Math.Max(titleScale, pageTitleScale);
+ var contentScale = Math.Clamp(
+ 1d + ((renderScale - 1d) * 0.7d) + ((typographyScale - 1d) * 0.45d),
+ 1d,
+ 1.45d);
+
+ var horizontalMargin = Math.Clamp(16d * renderScale, 12d, 32d);
+ var topMargin = Math.Clamp(2d * renderScale, 0d, 8d);
+ var bottomMargin = Math.Clamp(16d * renderScale, 12d, 28d);
+ var columnSpacing = Math.Clamp(20d * renderScale, 12d, 28d);
+ var drawerWidth = Math.Clamp(BaseDrawerWidth * contentScale, 276d, 380d);
+ var compactPaneWidth = Math.Clamp(48d * renderScale, 40d, 60d);
+ var narrowThreshold = Math.Clamp(BaseNarrowThreshold * renderScale, 760d, 980d);
+ var isNarrow = width < narrowThreshold;
+ var paneReservedWidth = GetReservedPaneWidth(compactPaneWidth, isNarrow);
+
+ SettingsContentGrid.Margin = new Thickness(horizontalMargin, topMargin, horizontalMargin, bottomMargin);
+ DrawerBorder.Width = drawerWidth;
+
+ if (isNarrow)
+ {
+ SettingsContentGrid.ColumnDefinitions = new ColumnDefinitions("*");
+ SettingsContentGrid.ColumnSpacing = 0;
+ if (DrawerBorder.IsVisible)
+ {
+ ViewModel.IsDrawerOpen = false;
+ }
+
+ DrawerBorder.IsVisible = false;
+ }
+ else
+ {
+ SettingsContentGrid.ColumnDefinitions = new ColumnDefinitions("*,Auto");
+ SettingsContentGrid.ColumnSpacing = columnSpacing;
+ }
+
+ var contentHostWidth = ContentFrame?.Bounds.Width > 1
+ ? ContentFrame.Bounds.Width
+ : 0d;
+ if (contentHostWidth <= 1)
+ {
+ var rootContentWidth = RootNavigationView?.Bounds.Width > 1
+ ? RootNavigationView.Bounds.Width - paneReservedWidth
+ : SettingsContentGrid.Bounds.Width;
+ contentHostWidth = rootContentWidth - (isNarrow ? 0d : drawerWidth + SettingsContentGrid.ColumnSpacing);
+ }
+
+ contentHostWidth = Math.Max(MinSettingsContentWidth, contentHostWidth);
+
+ var edgePadding = Math.Clamp(24d * renderScale, 14d, 40d);
+ var preferredContentWidth = Math.Clamp(BaseSettingsContentWidth * contentScale, 820d, MaxSettingsContentWidth);
+ var availableContentWidth = Math.Max(MinSettingsContentWidth, contentHostWidth - edgePadding * 2d);
+ var resolvedContentWidth = availableContentWidth > preferredContentWidth
+ ? preferredContentWidth
+ : availableContentWidth;
+
+ Resources["SettingsPageContentWidth"] = resolvedContentWidth;
+
+ if (PageTitleContainer is not null)
+ {
+ PageTitleContainer.Width = resolvedContentWidth;
+ PageTitleContainer.HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center;
+ }
+
+ if (PageTitleTextBlock is not null)
+ {
+ var narrowTitleThreshold = Math.Clamp(760d * renderScale, 700d, 860d);
+ PageTitleTextBlock.Classes.Set("narrow", resolvedContentWidth < narrowTitleThreshold);
+ }
+
+ return true;
+ }
+
+ private void UpdateResponsiveLayout()
+ {
+ _ = TryApplyResponsiveLayout();
+ return;
+
+ if (SettingsContentGrid is null || DrawerBorder is null)
+ {
+ return;
+ }
+
+ var width = Bounds.Width;
+ const double narrowThreshold = 800;
+
+ var isNarrow = width < narrowThreshold;
+
+ // 小窗口时隐藏抽屉面板
+ if (isNarrow)
+ {
+ SettingsContentGrid.ColumnDefinitions = new ColumnDefinitions("*");
+ SettingsContentGrid.ColumnSpacing = 0;
+ if (DrawerBorder.IsVisible)
+ {
+ ViewModel.IsDrawerOpen = false;
+ }
+ DrawerBorder.IsVisible = false;
+ }
+ else
+ {
+ SettingsContentGrid.ColumnDefinitions = new ColumnDefinitions("*,Auto");
+ SettingsContentGrid.ColumnSpacing = 20;
+ }
}
private void OnClosed(object? sender, EventArgs e)
{
_cachedPages.Clear();
PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged;
+ if (RootNavigationView is not null)
+ {
+ RootNavigationView.PropertyChanged -= OnRootNavigationViewPropertyChanged;
+ }
Opened -= OnOpened;
SizeChanged -= OnWindowSizeChanged;
}
@@ -322,6 +470,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
RootNavigationView.IsPaneOpen = !RootNavigationView.IsPaneOpen;
UpdatePaneToggleIcon();
+ UpdateResponsiveLayout();
+ RequestResponsiveLayoutRefresh();
}
private void OnCloseWindowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -331,6 +481,46 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
Close();
}
+ private void OnRootNavigationViewPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ _ = sender;
+
+ if (e.Property == NavigationView.IsPaneOpenProperty ||
+ e.Property == NavigationView.OpenPaneLengthProperty ||
+ e.Property == NavigationView.PaneDisplayModeProperty)
+ {
+ UpdatePaneToggleIcon();
+ RequestResponsiveLayoutRefresh();
+ }
+ }
+
+ private void RequestResponsiveLayoutRefresh()
+ {
+ if (_isResponsiveLayoutRefreshPending)
+ {
+ return;
+ }
+
+ _isResponsiveLayoutRefreshPending = true;
+ Dispatcher.UIThread.Post(() =>
+ {
+ _isResponsiveLayoutRefreshPending = false;
+ UpdateResponsiveLayout();
+ }, DispatcherPriority.Background);
+ }
+
+ private double GetReservedPaneWidth(double compactPaneWidth, bool isNarrow)
+ {
+ if (RootNavigationView is null || isNarrow)
+ {
+ return 0d;
+ }
+
+ return RootNavigationView.IsPaneOpen
+ ? RootNavigationView.OpenPaneLength
+ : compactPaneWidth;
+ }
+
private void UpdatePaneToggleIcon()
{
if (TogglePaneButtonIcon is null || RootNavigationView is null)
@@ -457,6 +647,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
return iconKey?.Trim() switch
{
"DesignIdeas" => Symbol.Color,
+ "Image" => Symbol.Image,
"GridDots" => Symbol.GridDots,
"PuzzlePiece" => Symbol.PuzzlePiece,
"Info" => Symbol.Info,