settings_re5

This commit is contained in:
lincube
2026-03-14 13:36:18 +08:00
parent 5fdaa2539b
commit e8be0f0576
24 changed files with 993 additions and 321 deletions

View File

@@ -8,6 +8,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading; using Avalonia.Threading;
@@ -17,6 +18,7 @@ using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Theme;
using LanMountainDesktop.ViewModels; using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views; using LanMountainDesktop.Views;
@@ -24,6 +26,7 @@ namespace LanMountainDesktop;
public partial class App : Application public partial class App : Application
{ {
private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6");
private enum DesktopShellState private enum DesktopShellState
{ {
ForegroundDesktop = 0, ForegroundDesktop = 0,
@@ -75,13 +78,18 @@ public partial class App : Application
PageId: pageTag)); PageId: pageTag));
} }
public App()
{
_settingsFacade.Settings.Changed += OnSettingsChanged;
}
public override void Initialize() public override void Initialize()
{ {
AppLogger.Info("App", "Initializing application resources."); AppLogger.Info("App", "Initializing application resources.");
ConfigureWebViewUserDataFolder(); ConfigureWebViewUserDataFolder();
AvaloniaWebViewBuilder.Initialize(default); AvaloniaWebViewBuilder.Initialize(default);
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
ApplyInitialThemeVariantFromSettings(); ApplyThemeFromSettings();
ApplyCurrentCultureFromSettings(); ApplyCurrentCultureFromSettings();
EnsureSettingsWindowService(); EnsureSettingsWindowService();
} }
@@ -292,12 +300,13 @@ public partial class App : Application
_settingsFacade); _settingsFacade);
} }
private void ApplyInitialThemeVariantFromSettings() private void ApplyThemeFromSettings()
{ {
var themeState = _settingsFacade.Theme.Get(); var themeState = _settingsFacade.Theme.Get();
RequestedThemeVariant = themeState.IsNightMode RequestedThemeVariant = themeState.IsNightMode
? ThemeVariant.Dark ? ThemeVariant.Dark
: ThemeVariant.Light; : ThemeVariant.Light;
ApplyAdaptiveThemeResources(themeState);
} }
private void ApplyCurrentCultureFromSettings() private void ApplyCurrentCultureFromSettings()
@@ -424,7 +433,7 @@ public partial class App : Application
{ {
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
ApplyInitialThemeVariantFromSettings(); ApplyThemeFromSettings();
ApplyCurrentCultureFromSettings(); ApplyCurrentCultureFromSettings();
if (_trayIcons is not null) if (_trayIcons is not null)
{ {
@@ -433,6 +442,71 @@ public partial class App : Application
}, DispatcherPriority.Background); }, 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() private void RegisterUiUnhandledExceptionGuard()
{ {
if (_uiUnhandledExceptionHooked) if (_uiUnhandledExceptionHooked)
@@ -479,6 +553,7 @@ public partial class App : Application
_exitCleanupCompleted = true; _exitCleanupCompleted = true;
AppSettingsService.SettingsSaved -= OnAppSettingsSaved; AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
_settingsFacade.Settings.Changed -= OnSettingsChanged;
try try
{ {

View File

@@ -0,0 +1,19 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fi="using:FluentIcons.Avalonia"
x:Class="LanMountainDesktop.Controls.IconText">
<StackPanel Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<fi:FluentIcon x:Name="IconElement"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
FontSize="14"
VerticalAlignment="Center" />
<TextBlock x:Name="TextElement"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
FontSize="14"
FontWeight="Medium"
VerticalAlignment="Center" />
</StackPanel>
</UserControl>

View File

@@ -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<Icon> IconProperty =
AvaloniaProperty.Register<IconText, Icon>(nameof(Icon), Icon.Info);
public static readonly StyledProperty<string> TextProperty =
AvaloniaProperty.Register<IconText, string>(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<Icon>();
}
}
else if (change.Property == TextProperty)
{
if (TextElement is not null)
{
TextElement.Text = change.GetNewValue<string?>();
}
}
}
}

View File

@@ -102,6 +102,7 @@ public partial class SettingsOptionCard : UserControl
return iconKey?.Trim() switch return iconKey?.Trim() switch
{ {
"DesignIdeas" => Symbol.Color, "DesignIdeas" => Symbol.Color,
"Image" => Symbol.Image,
"GridDots" => Symbol.GridDots, "GridDots" => Symbol.GridDots,
"PuzzlePiece" => Symbol.PuzzlePiece, "PuzzlePiece" => Symbol.PuzzlePiece,
"Info" => Symbol.Info, "Info" => Symbol.Info,

View File

@@ -87,6 +87,7 @@ public partial class SettingsSectionCard : UserControl
return iconKey?.Trim() switch return iconKey?.Trim() switch
{ {
"DesignIdeas" => Symbol.Color, "DesignIdeas" => Symbol.Color,
"Image" => Symbol.Image,
"GridDots" => Symbol.GridDots, "GridDots" => Symbol.GridDots,
"PuzzlePiece" => Symbol.PuzzlePiece, "PuzzlePiece" => Symbol.PuzzlePiece,
"Info" => Symbol.Info, "Info" => Symbol.Info,

View File

@@ -232,7 +232,7 @@
"settings.general.preview_date_label": "Date", "settings.general.preview_date_label": "Date",
"settings.general.render_mode_restart_message": "Rendering mode changes require restarting the app.", "settings.general.render_mode_restart_message": "Rendering mode changes require restarting the app.",
"settings.appearance.title": "Appearance", "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.appearance.theme_header": "Theme",
"settings.color.enable_night_mode_toggle": "Enable night mode", "settings.color.enable_night_mode_toggle": "Enable night mode",
"settings.color.use_system_chrome_toggle": "Use system window chrome", "settings.color.use_system_chrome_toggle": "Use system window chrome",
@@ -802,4 +802,3 @@
"single_instance.notice.button": "OK" "single_instance.notice.button": "OK"
} }

View File

@@ -232,7 +232,7 @@
"settings.general.preview_date_label": "日期", "settings.general.preview_date_label": "日期",
"settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。", "settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。",
"settings.appearance.title": "外观", "settings.appearance.title": "外观",
"settings.appearance.description": "切换主题、壁纸和状态栏展示。", "settings.appearance.description": "切换主题状态栏展示。",
"settings.appearance.theme_header": "主题", "settings.appearance.theme_header": "主题",
"settings.color.enable_night_mode_toggle": "启用夜间模式", "settings.color.enable_night_mode_toggle": "启用夜间模式",
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏", "settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
@@ -802,4 +802,3 @@
"single_instance.notice.button": "确定" "single_instance.notice.button": "确定"
} }

View File

@@ -2,11 +2,13 @@ using System;
using System.Linq; using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading; using Avalonia.Threading;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
using LanMountainDesktop.ViewModels; using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views; using LanMountainDesktop.Views;
@@ -50,6 +52,7 @@ public interface ISettingsWindowService
internal sealed class SettingsWindowService : ISettingsWindowService internal sealed class SettingsWindowService : ISettingsWindowService
{ {
private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6");
private readonly ISettingsPageRegistry _pageRegistry; private readonly ISettingsPageRegistry _pageRegistry;
private readonly IHostApplicationLifecycle _hostApplicationLifecycle; private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
private readonly ISettingsFacadeService _settingsFacade; private readonly ISettingsFacadeService _settingsFacade;
@@ -85,7 +88,7 @@ internal sealed class SettingsWindowService : ISettingsWindowService
_window ??= CreateWindow(); _window ??= CreateWindow();
var themeState = _settingsFacade.Theme.Get(); var themeState = _settingsFacade.Theme.Get();
_window.ApplyChromeMode(themeState.UseSystemChrome); _window.ApplyChromeMode(themeState.UseSystemChrome);
ApplyTheme(_window, themeState.IsNightMode); ApplyTheme(_window, themeState);
_window.ReloadPages(request.PageId); _window.ReloadPages(request.PageId);
PositionWindow(_window, request); PositionWindow(_window, request);
@@ -140,7 +143,7 @@ internal sealed class SettingsWindowService : ISettingsWindowService
_pageRegistry, _pageRegistry,
_hostApplicationLifecycle, _hostApplicationLifecycle,
useSystemChrome); useSystemChrome);
ApplyTheme(window, themeState.IsNightMode); ApplyTheme(window, themeState);
window.ShowInTaskbar = false; window.ShowInTaskbar = false;
window.Closed += (_, _) => window.Closed += (_, _) =>
{ {
@@ -277,6 +280,7 @@ internal sealed class SettingsWindowService : ISettingsWindowService
var themeChanged = var themeChanged =
refreshAll || refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) || changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase); changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase);
if (languageChanged) if (languageChanged)
@@ -292,15 +296,40 @@ internal sealed class SettingsWindowService : ISettingsWindowService
{ {
var themeState = _settingsFacade.Theme.Get(); var themeState = _settingsFacade.Theme.Get();
_window.ApplyChromeMode(themeState.UseSystemChrome); _window.ApplyChromeMode(themeState.UseSystemChrome);
ApplyTheme(_window, themeState.IsNightMode); ApplyTheme(_window, themeState);
} }
}, DispatcherPriority.Background); }, 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.Dark
: ThemeVariant.Light; : 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;
} }
} }

View File

@@ -107,11 +107,21 @@
</Style> </Style>
<Style Selector=".settings-scope ComboBoxItem"> <Style Selector=".settings-scope ComboBoxItem">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" /> <Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" />
<Setter Property="Padding" Value="10,6" /> <Setter Property="Padding" Value="10,6" />
<Setter Property="Margin" Value="4,2" /> <Setter Property="Margin" Value="4,2" />
</Style> </Style>
<Style Selector=".settings-scope ComboBoxItem:pointerover">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonHoverBackgroundBrush}" />
</Style>
<Style Selector=".settings-scope ComboBoxItem:selected">
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
</Style>
<Style Selector=".settings-scope ui|NumberBox"> <Style Selector=".settings-scope ui|NumberBox">
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" /> <Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
<Setter Property="MinHeight" Value="34" /> <Setter Property="MinHeight" Value="34" />
@@ -139,6 +149,10 @@
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" /> <Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
</Style> </Style>
<Style Selector=".settings-scope ui|NavigationView, .settings-scope ui|NavigationViewItem, .settings-scope ui|SettingsExpander, .settings-scope ui|InfoBar, .settings-scope ListBoxItem">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
<Style Selector="Button.swatch-button"> <Style Selector="Button.swatch-button">
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="16" /> <Setter Property="CornerRadius" Value="16" />

View File

@@ -4,9 +4,9 @@
<Style Selector="StackPanel.settings-page-container"> <Style Selector="StackPanel.settings-page-container">
<Setter Property="Spacing" Value="0" /> <Setter Property="Spacing" Value="0" />
<Setter Property="Margin" Value="12,14,28,32" /> <Setter Property="Margin" Value="0,12,0,24" />
<Setter Property="MaxWidth" Value="900" /> <Setter Property="Width" Value="{DynamicResource SettingsPageContentWidth}" />
<Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="HorizontalAlignment" Value="Center" />
</Style> </Style>
<Style Selector="TextBlock.settings-section-title"> <Style Selector="TextBlock.settings-section-title">
@@ -20,7 +20,6 @@
<Setter Property="FontSize" Value="14" /> <Setter Property="FontSize" Value="14" />
<Setter Property="Opacity" Value="0.76" /> <Setter Property="Opacity" Value="0.76" />
<Setter Property="TextWrapping" Value="Wrap" /> <Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="MaxWidth" Value="760" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" /> <Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="Margin" Value="0,0,0,22" /> <Setter Property="Margin" Value="0,0,0,22" />
</Style> </Style>
@@ -33,6 +32,7 @@
</Style> </Style>
<Style Selector="Border.settings-section-card, Border.settings-option-card, Border.settings-list-item"> <Style Selector="Border.settings-section-card, Border.settings-option-card, Border.settings-list-item">
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="Transitions"> <Setter Property="Transitions">
@@ -48,7 +48,6 @@
</Style> </Style>
<Style Selector="Border.settings-section-card"> <Style Selector="Border.settings-section-card">
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
<Setter Property="CornerRadius" Value="18" /> <Setter Property="CornerRadius" Value="18" />
<Setter Property="Padding" Value="20" /> <Setter Property="Padding" Value="20" />
<Setter Property="Margin" Value="0,0,0,14" /> <Setter Property="Margin" Value="0,0,0,14" />
@@ -56,7 +55,6 @@
</Style> </Style>
<Style Selector="Border.settings-option-card"> <Style Selector="Border.settings-option-card">
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
<Setter Property="CornerRadius" Value="14" /> <Setter Property="CornerRadius" Value="14" />
<Setter Property="Padding" Value="16" /> <Setter Property="Padding" Value="16" />
<Setter Property="Margin" Value="0,0,0,12" /> <Setter Property="Margin" Value="0,0,0,12" />
@@ -64,7 +62,6 @@
</Style> </Style>
<Style Selector="Border.settings-list-item"> <Style Selector="Border.settings-list-item">
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
<Setter Property="CornerRadius" Value="14" /> <Setter Property="CornerRadius" Value="14" />
<Setter Property="Padding" Value="16" /> <Setter Property="Padding" Value="16" />
<Setter Property="Margin" Value="0,0,0,10" /> <Setter Property="Margin" Value="0,0,0,10" />
@@ -121,7 +118,6 @@
<Setter Property="Opacity" Value="0.68" /> <Setter Property="Opacity" Value="0.68" />
<Setter Property="TextWrapping" Value="Wrap" /> <Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" /> <Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="MaxWidth" Value="620" />
</Style> </Style>
<Style Selector="StackPanel.settings-item"> <Style Selector="StackPanel.settings-item">
@@ -137,6 +133,11 @@
<Setter Property="ColumnSpacing" Value="12" /> <Setter Property="ColumnSpacing" Value="12" />
</Style> </Style>
<Style Selector="ui|SettingsExpander">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="MinWidth" Value="0" />
</Style>
<Style Selector="ui|SettingsExpander.settings-expander-card"> <Style Selector="ui|SettingsExpander.settings-expander-card">
<Setter Property="Margin" Value="0,0,0,14" /> <Setter Property="Margin" Value="0,0,0,14" />
</Style> </Style>

View File

@@ -41,6 +41,9 @@ public sealed partial class SettingsWindowViewModel : ViewModelBase
[ObservableProperty] [ObservableProperty]
private string _currentPageTitle = string.Empty; private string _currentPageTitle = string.Empty;
[ObservableProperty]
private bool _isPageTitleVisible = true;
[ObservableProperty] [ObservableProperty]
private string? _currentPageDescription; private string? _currentPageDescription;
@@ -636,7 +639,7 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
private void RefreshLocalizedText() private void RefreshLocalizedText()
{ {
PageTitle = L("settings.appearance.title", "Appearance"); 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"); ThemeHeader = L("settings.appearance.theme_header", "Theme");
NightModeLabel = L("settings.color.enable_night_mode_toggle", "Enable night mode"); NightModeLabel = L("settings.color.enable_night_mode_toggle", "Enable night mode");
UseSystemChromeLabel = L("settings.color.use_system_chrome_toggle", "Use system window chrome"); UseSystemChromeLabel = L("settings.color.use_system_chrome_toggle", "Use system window chrome");

View File

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

View File

@@ -13,6 +13,7 @@ using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components; using LanMountainDesktop.Views.Components;
namespace LanMountainDesktop.Views; namespace LanMountainDesktop.Views;
@@ -193,6 +194,30 @@ public partial class MainWindow
return false; 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) private void TryRestoreWallpaper(string? savedWallpaperPath)
{ {
_wallpaperPath = string.IsNullOrWhiteSpace(savedWallpaperPath) ? null : savedWallpaperPath; _wallpaperPath = string.IsNullOrWhiteSpace(savedWallpaperPath) ? null : savedWallpaperPath;
@@ -285,6 +310,9 @@ public partial class MainWindow
Application.Current.RequestedThemeVariant = requestedThemeVariant; Application.Current.RequestedThemeVariant = requestedThemeVariant;
} }
ApplyAdaptiveThemeResources();
ApplyWallpaperBrush();
if (!refreshPalettes) if (!refreshPalettes)
{ {
return; return;

View File

@@ -25,43 +25,6 @@
<Design.DataContext> <Design.DataContext>
<vm:MainWindowViewModel /> <vm:MainWindowViewModel />
</Design.DataContext> </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" />
<SolidColorBrush x:Key="AdaptiveButtonPressedBackgroundBrush" Color="#AA2A3B55" />
<SolidColorBrush x:Key="AdaptiveGlassPanelBackgroundBrush" Color="#70233448" />
<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>
<Grid> <Grid>
<Grid x:Name="DesktopPage" <Grid x:Name="DesktopPage"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"

View File

@@ -3,78 +3,102 @@
xmlns:vm="using:LanMountainDesktop.ViewModels" xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:controls="using:LanMountainDesktop.Controls" xmlns:controls="using:LanMountainDesktop.Controls"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage" x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage"
x:DataType="vm:AboutSettingsPageViewModel"> x:DataType="vm:AboutSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container"> <StackPanel Classes="settings-page-container">
<TextBlock Classes="settings-section-title"
Text="{Binding PageTitle}" />
<TextBlock Classes="settings-section-description"
Text="{Binding PageDescription}" />
<controls:SettingsSectionCard IconKey="Info" <!-- 应用信息分组 -->
Title="{Binding AppInfoHeader}"> <controls:IconText Icon="Info"
<controls:SettingsSectionCard.CardContent> Text="{Binding AppInfoHeader}"
<StackPanel Spacing="14"> Margin="0,0,0,4" />
<StackPanel Classes="settings-item">
<TextBlock Classes="settings-item-label"
Text="{Binding VersionLabel}" />
<TextBlock Opacity="0.82"
Text="{Binding VersionText}" />
</StackPanel>
<StackPanel Classes="settings-item"> <ui:SettingsExpander Header="{Binding AppInfoHeader}">
<TextBlock Classes="settings-item-label" <ui:SettingsExpander.IconSource>
Text="{Binding RenderBackendLabel}" /> <fi:SymbolIconSource Symbol="Info" />
<TextBlock Opacity="0.82" </ui:SettingsExpander.IconSource>
Text="{Binding RenderBackendText}" /> <ui:SettingsExpanderItem>
</StackPanel> <Grid ColumnDefinitions="Auto,*">
</StackPanel> <TextBlock Text="{Binding VersionLabel}"
</controls:SettingsSectionCard.CardContent> VerticalAlignment="Center" />
</controls:SettingsSectionCard> <TextBlock Grid.Column="1"
HorizontalAlignment="Right"
Opacity="0.82"
Text="{Binding VersionText}" />
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="{Binding RenderBackendLabel}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Right"
Opacity="0.82"
Text="{Binding RenderBackendText}" />
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<ui:SettingsExpander Classes="settings-expander-card" <Separator Classes="settings-separator" />
Header="{Binding UpdateHeader}"
<!-- 更新设置分组 -->
<controls:IconText Icon="ArrowSync"
Text="{Binding UpdateHeader}"
Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding UpdateHeader}"
IsExpanded="True"> IsExpanded="True">
<StackPanel Spacing="12"> <ui:SettingsExpander.IconSource>
<controls:SettingsOptionCard IconKey="ArrowSync" <fi:SymbolIconSource Symbol="ArrowSync" />
Title="{Binding AutoCheckUpdatesLabel}"> </ui:SettingsExpander.IconSource>
<controls:SettingsOptionCard.ActionContent> <ui:SettingsExpanderItem>
<ToggleSwitch IsChecked="{Binding AutoCheckUpdates}" /> <Grid ColumnDefinitions="Auto,*">
</controls:SettingsOptionCard.ActionContent> <TextBlock Text="{Binding AutoCheckUpdatesLabel}"
</controls:SettingsOptionCard> VerticalAlignment="Center" />
<ToggleSwitch Grid.Column="1"
<controls:SettingsOptionCard IconKey="ArrowSync" HorizontalAlignment="Right"
Title="{Binding IncludePrereleaseUpdatesLabel}"> IsChecked="{Binding AutoCheckUpdates}" />
<controls:SettingsOptionCard.ActionContent> </Grid>
<ToggleSwitch IsChecked="{Binding IncludePrereleaseUpdates}" /> </ui:SettingsExpanderItem>
</controls:SettingsOptionCard.ActionContent> <ui:SettingsExpanderItem>
</controls:SettingsOptionCard> <Grid ColumnDefinitions="Auto,*">
<TextBlock Text="{Binding IncludePrereleaseUpdatesLabel}"
<controls:SettingsOptionCard IconKey="ArrowSync" VerticalAlignment="Center" />
Title="{Binding UpdateChannelLabel}"> <ToggleSwitch Grid.Column="1"
<controls:SettingsOptionCard.DetailsContent> HorizontalAlignment="Right"
<ComboBox ItemsSource="{Binding UpdateChannels}" IsChecked="{Binding IncludePrereleaseUpdates}" />
SelectedItem="{Binding SelectedUpdateChannel}"> </Grid>
<ComboBox.ItemTemplate> </ui:SettingsExpanderItem>
<DataTemplate x:DataType="vm:SelectionOption"> <ui:SettingsExpanderItem>
<TextBlock Text="{Binding Label}" /> <Grid ColumnDefinitions="Auto,*">
</DataTemplate> <TextBlock Text="{Binding UpdateChannelLabel}"
</ComboBox.ItemTemplate> VerticalAlignment="Center" />
</ComboBox> <ComboBox Grid.Column="1"
</controls:SettingsOptionCard.DetailsContent> MinWidth="160"
</controls:SettingsOptionCard> HorizontalAlignment="Right"
ItemsSource="{Binding UpdateChannels}"
<StackPanel Orientation="Horizontal" SelectedItem="{Binding SelectedUpdateChannel}">
Spacing="12"> <ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<StackPanel Spacing="8">
<Button Command="{Binding CheckForUpdatesCommand}" <Button Command="{Binding CheckForUpdatesCommand}"
Content="{Binding CheckForUpdatesButtonText}" /> Content="{Binding CheckForUpdatesButtonText}" />
<TextBlock VerticalAlignment="Center" <TextBlock Opacity="0.76"
Opacity="0.76" FontSize="12"
TextWrapping="Wrap"
Text="{Binding UpdateStatus}" /> Text="{Binding UpdateStatus}" />
</StackPanel> </StackPanel>
</StackPanel> </ui:SettingsExpanderItem>
</ui:SettingsExpander> </ui:SettingsExpander>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View File

@@ -2,96 +2,79 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels" xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:controls="using:LanMountainDesktop.Controls" xmlns:controls="using:LanMountainDesktop.Controls"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.AppearanceSettingsPage" x:Class="LanMountainDesktop.Views.SettingsPages.AppearanceSettingsPage"
x:DataType="vm:AppearanceSettingsPageViewModel"> x:DataType="vm:AppearanceSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container"> <StackPanel Classes="settings-page-container">
<TextBlock Classes="settings-section-title"
Text="{Binding PageTitle}" />
<TextBlock Classes="settings-section-description"
Text="{Binding PageDescription}" />
<controls:SettingsOptionCard IconKey="DesignIdeas" <controls:IconText Icon="Color"
Title="{Binding NightModeLabel}"> Text="{Binding ThemeHeader}"
<controls:SettingsOptionCard.ActionContent> Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding NightModeLabel}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="WeatherMoon" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding IsNightMode}" /> <ToggleSwitch IsChecked="{Binding IsNightMode}" />
</controls:SettingsOptionCard.ActionContent> </ui:SettingsExpander.Footer>
</controls:SettingsOptionCard> </ui:SettingsExpander>
<controls:SettingsOptionCard IconKey="DesignIdeas" <ui:SettingsExpander Header="{Binding UseSystemChromeLabel}">
Title="{Binding UseSystemChromeLabel}"> <ui:SettingsExpander.IconSource>
<controls:SettingsOptionCard.ActionContent> <fi:SymbolIconSource Symbol="Window" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding UseSystemChrome}" /> <ToggleSwitch IsChecked="{Binding UseSystemChrome}" />
</controls:SettingsOptionCard.ActionContent> </ui:SettingsExpander.Footer>
</controls:SettingsOptionCard> </ui:SettingsExpander>
<controls:SettingsOptionCard IconKey="DesignIdeas" <ui:SettingsExpander Header="{Binding ThemeColorLabel}">
Title="{Binding ThemeColorLabel}"> <ui:SettingsExpander.IconSource>
<controls:SettingsOptionCard.DetailsContent> <fi:SymbolIconSource Symbol="Color" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<TextBox Watermark="#AABBCC" <TextBox Watermark="#AABBCC"
MinWidth="160"
Text="{Binding ThemeColor}" /> Text="{Binding ThemeColor}" />
</controls:SettingsOptionCard.DetailsContent> </ui:SettingsExpander.Footer>
</controls:SettingsOptionCard> </ui:SettingsExpander>
<controls:SettingsSectionCard IconKey="DesignIdeas" <Separator Classes="settings-separator" />
Title="{Binding WallpaperHeader}">
<controls:SettingsSectionCard.CardContent>
<StackPanel Spacing="14">
<StackPanel Classes="settings-item">
<TextBlock Classes="settings-item-label"
Text="{Binding WallpaperPathLabel}" />
<TextBox IsReadOnly="True"
Text="{Binding WallpaperPath}" />
</StackPanel>
<StackPanel Classes="settings-item"> <controls:IconText Icon="Clock"
<TextBlock Classes="settings-item-label" Text="{Binding ClockHeader}"
Text="{Binding WallpaperPlacementLabel}" /> Margin="0,0,0,4" />
<ComboBox ItemsSource="{Binding WallpaperPlacements}"
SelectedItem="{Binding SelectedWallpaperPlacement}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<Button HorizontalAlignment="Left" <ui:SettingsExpander Header="{Binding ClockHeader}"
Click="OnBrowseWallpaperClick" Description="{Binding ClockDescription}">
Content="{Binding ImportWallpaperButtonText}" /> <ui:SettingsExpander.IconSource>
</StackPanel> <fi:SymbolIconSource Symbol="Clock" />
</controls:SettingsSectionCard.CardContent> </ui:SettingsExpander.IconSource>
</controls:SettingsSectionCard> <ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding ShowClock}" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="{Binding ClockFormatLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
MinWidth="160"
HorizontalAlignment="Right"
ItemsSource="{Binding ClockFormats}"
SelectedItem="{Binding SelectedClockFormat}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<controls:SettingsSectionCard IconKey="DesignIdeas"
Title="{Binding ClockHeader}"
Description="{Binding ClockDescription}">
<controls:SettingsSectionCard.CardContent>
<StackPanel Spacing="14">
<controls:SettingsOptionCard IconKey="DesignIdeas"
Title="{Binding ClockHeader}">
<controls:SettingsOptionCard.ActionContent>
<ToggleSwitch IsChecked="{Binding ShowClock}" />
</controls:SettingsOptionCard.ActionContent>
</controls:SettingsOptionCard>
<controls:SettingsOptionCard IconKey="DesignIdeas"
Title="{Binding ClockFormatLabel}">
<controls:SettingsOptionCard.DetailsContent>
<ComboBox ItemsSource="{Binding ClockFormats}"
SelectedItem="{Binding SelectedClockFormat}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:SettingsOptionCard.DetailsContent>
</controls:SettingsOptionCard>
</StackPanel>
</controls:SettingsSectionCard.CardContent>
</controls:SettingsSectionCard>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View File

@@ -1,9 +1,6 @@
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels; using LanMountainDesktop.ViewModels;
using System.Linq;
namespace LanMountainDesktop.Views.SettingsPages; namespace LanMountainDesktop.Views.SettingsPages;
@@ -30,25 +27,4 @@ public partial class AppearanceSettingsPage : SettingsPageBase
} }
public AppearanceSettingsPageViewModel ViewModel { get; } 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);
}
}
} }

View File

@@ -1,40 +1,56 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels" xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:controls="using:LanMountainDesktop.Controls"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.ComponentsSettingsPage" x:Class="LanMountainDesktop.Views.SettingsPages.ComponentsSettingsPage"
x:DataType="vm:ComponentsSettingsPageViewModel"> x:DataType="vm:ComponentsSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container"> <StackPanel Classes="settings-page-container">
<TextBlock Classes="settings-section-title"
Text="{Binding PageTitle}" />
<TextBlock Classes="settings-section-description"
Text="{Binding PageDescription}" />
<ui:SettingsExpander Classes="settings-expander-card" <!-- 网格布局设置分组 -->
Header="{Binding GridHeader}" <controls:IconText Icon="GridDots"
Text="{Binding GridHeader}"
Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding GridHeader}"
IsExpanded="True"> IsExpanded="True">
<StackPanel Spacing="14"> <ui:SettingsExpander.IconSource>
<StackPanel Classes="settings-item"> <fi:SymbolIconSource Symbol="GridDots" />
<TextBlock Classes="settings-item-label" </ui:SettingsExpander.IconSource>
Text="{Binding ShortSideCellsLabel}" /> <ui:SettingsExpanderItem>
<NumericUpDown Minimum="6" <Grid ColumnDefinitions="Auto,*">
<TextBlock Text="{Binding ShortSideCellsLabel}"
VerticalAlignment="Center" />
<NumericUpDown Grid.Column="1"
MinWidth="120"
HorizontalAlignment="Right"
Minimum="6"
Maximum="96" Maximum="96"
Value="{Binding ShortSideCells}" /> Value="{Binding ShortSideCells}" />
</StackPanel> </Grid>
</ui:SettingsExpanderItem>
<StackPanel Classes="settings-item"> <ui:SettingsExpanderItem>
<TextBlock Classes="settings-item-label" <Grid ColumnDefinitions="Auto,*">
Text="{Binding EdgeInsetPercentLabel}" /> <TextBlock Text="{Binding EdgeInsetPercentLabel}"
<NumericUpDown Minimum="0" VerticalAlignment="Center" />
<NumericUpDown Grid.Column="1"
MinWidth="120"
HorizontalAlignment="Right"
Minimum="0"
Maximum="30" Maximum="30"
Value="{Binding EdgeInsetPercent}" /> Value="{Binding EdgeInsetPercent}" />
</StackPanel> </Grid>
</ui:SettingsExpanderItem>
<StackPanel Classes="settings-item"> <ui:SettingsExpanderItem>
<TextBlock Classes="settings-item-label" <Grid ColumnDefinitions="Auto,*">
Text="{Binding SpacingPresetLabel}" /> <TextBlock Text="{Binding SpacingPresetLabel}"
<ComboBox ItemsSource="{Binding SpacingPresets}" VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
MinWidth="160"
HorizontalAlignment="Right"
ItemsSource="{Binding SpacingPresets}"
SelectedItem="{Binding SelectedSpacingPreset}"> SelectedItem="{Binding SelectedSpacingPreset}">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption"> <DataTemplate x:DataType="vm:SelectionOption">
@@ -42,9 +58,10 @@
</DataTemplate> </DataTemplate>
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
</StackPanel> </Grid>
</StackPanel> </ui:SettingsExpanderItem>
</ui:SettingsExpander> </ui:SettingsExpander>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View File

@@ -3,18 +3,22 @@
xmlns:vm="using:LanMountainDesktop.ViewModels" xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:controls="using:LanMountainDesktop.Controls" xmlns:controls="using:LanMountainDesktop.Controls"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.GeneralSettingsPage" x:Class="LanMountainDesktop.Views.SettingsPages.GeneralSettingsPage"
x:DataType="vm:GeneralSettingsPageViewModel"> x:DataType="vm:GeneralSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container"> <StackPanel Classes="settings-page-container">
<TextBlock Classes="settings-section-title"
Text="{Binding PageTitle}" />
<TextBlock Classes="settings-section-description"
Text="{Binding PageDescription}" />
<controls:SettingsOptionCard IconKey="Settings" <!-- 区域设置分组 -->
Title="{Binding LanguageHeader}"> <controls:IconText Icon="Globe"
<controls:SettingsOptionCard.DetailsContent> Text="{Binding BasicHeader}"
Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding LanguageHeader}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Settings" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox MinWidth="240" <ComboBox MinWidth="240"
ItemsSource="{Binding Languages}" ItemsSource="{Binding Languages}"
SelectedItem="{Binding SelectedLanguage}"> SelectedItem="{Binding SelectedLanguage}">
@@ -24,13 +28,15 @@
</DataTemplate> </DataTemplate>
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
</controls:SettingsOptionCard.DetailsContent> </ui:SettingsExpander.Footer>
</controls:SettingsOptionCard> </ui:SettingsExpander>
<controls:SettingsOptionCard IconKey="Settings" <ui:SettingsExpander Header="{Binding TimeZoneHeader}"
Title="{Binding TimeZoneHeader}" Description="{Binding TimeZoneDescription}">
Description="{Binding TimeZoneDescription}"> <ui:SettingsExpander.IconSource>
<controls:SettingsOptionCard.DetailsContent> <fi:SymbolIconSource Symbol="Clock" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox MinWidth="280" <ComboBox MinWidth="280"
ItemsSource="{Binding TimeZones}" ItemsSource="{Binding TimeZones}"
SelectedItem="{Binding SelectedTimeZone}"> SelectedItem="{Binding SelectedTimeZone}">
@@ -40,12 +46,14 @@
</DataTemplate> </DataTemplate>
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
</controls:SettingsOptionCard.DetailsContent> </ui:SettingsExpander.Footer>
</controls:SettingsOptionCard> </ui:SettingsExpander>
<controls:SettingsSectionCard IconKey="Info" <ui:SettingsExpander Header="{Binding PreviewHeader}">
Title="{Binding PreviewHeader}"> <ui:SettingsExpander.IconSource>
<controls:SettingsSectionCard.CardContent> <fi:SymbolIconSource Symbol="Calendar" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*" <Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16" ColumnSpacing="16"
RowDefinitions="Auto,Auto" RowDefinitions="Auto,Auto"
@@ -63,13 +71,22 @@
Opacity="0.82" Opacity="0.82"
Text="{Binding PreviewDateText}" /> Text="{Binding PreviewDateText}" />
</Grid> </Grid>
</controls:SettingsSectionCard.CardContent> </ui:SettingsExpanderItem>
</controls:SettingsSectionCard> </ui:SettingsExpander>
<ui:SettingsExpander Classes="settings-expander-card" <Separator Classes="settings-separator" />
Header="{Binding RuntimeHeader}"
<!-- 运行时设置分组 -->
<controls:IconText Icon="DeveloperBoard"
Text="{Binding RuntimeHeader}"
Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding RenderModeHeader}"
Description="{Binding RuntimeDescription}" Description="{Binding RuntimeDescription}"
IsExpanded="True"> IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="DeveloperBoard" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<ComboBox MinWidth="240" <ComboBox MinWidth="240"
ItemsSource="{Binding RenderModes}" ItemsSource="{Binding RenderModes}"
@@ -81,7 +98,14 @@
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<TextBlock Text="{Binding RenderModeRestartMessage}"
Opacity="0.7"
FontSize="12"
TextWrapping="Wrap" />
</ui:SettingsExpanderItem>
</ui:SettingsExpander> </ui:SettingsExpander>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View File

@@ -1,87 +1,92 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels" 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.PluginsSettingsPage" x:Class="LanMountainDesktop.Views.SettingsPages.PluginsSettingsPage"
x:Name="Root" x:Name="Root"
x:DataType="vm:PluginsSettingsPageViewModel"> x:DataType="vm:PluginsSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container"> <StackPanel Classes="settings-page-container">
<TextBlock Classes="settings-section-title"
Text="{Binding PageTitle}" />
<TextBlock Classes="settings-section-description"
Text="{Binding PageDescription}" />
<StackPanel Orientation="Horizontal" <!-- 刷新按钮和状态 -->
Spacing="12" <ui:SettingsExpander Header="{Binding RefreshButtonText}"
Margin="0,0,0,18"> Description="{Binding StatusMessage}">
<Button Command="{Binding RefreshCommand}" <ui:SettingsExpander.IconSource>
Content="{Binding RefreshButtonText}" /> <fi:SymbolIconSource Symbol="ArrowSync" />
<TextBlock VerticalAlignment="Center" </ui:SettingsExpander.IconSource>
Opacity="0.76" <ui:SettingsExpander.Footer>
Text="{Binding StatusMessage}" /> <Button Command="{Binding RefreshCommand}"
</StackPanel> Content="{Binding RefreshButtonText}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<TextBlock Classes="settings-subsection-title" <Separator Classes="settings-separator" />
Text="{Binding InstalledHeader}" />
<!-- 已安装插件分组 -->
<controls:IconText Icon="PuzzleCube"
Text="{Binding InstalledHeader}"
Margin="0,0,0,4" />
<ItemsControl ItemsSource="{Binding InstalledPlugins}"> <ItemsControl ItemsSource="{Binding InstalledPlugins}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:InstalledPluginItemViewModel"> <DataTemplate x:DataType="vm:InstalledPluginItemViewModel">
<Border Classes="settings-list-item"> <ui:SettingsExpander>
<Grid Classes="settings-list-actions" <ui:SettingsExpander.IconSource>
ColumnDefinitions="*,Auto,Auto"> <fi:SymbolIconSource Symbol="PuzzleCube" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Header>
<StackPanel> <StackPanel>
<TextBlock FontWeight="SemiBold" <TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
Text="{Binding Name}" /> <TextBlock Opacity="0.76" FontSize="12" Text="{Binding Description}" />
<TextBlock Opacity="0.76"
FontSize="12"
Text="{Binding Description}" />
<TextBlock Opacity="0.58"
FontSize="11"
Text="{Binding Version}" />
</StackPanel> </StackPanel>
<ToggleSwitch Grid.Column="1" </ui:SettingsExpander.Header>
IsChecked="{Binding IsEnabled}" <ui:SettingsExpander.Footer>
Command="{Binding #Root.DataContext.TogglePluginCommand}" <StackPanel Orientation="Horizontal" Spacing="8">
CommandParameter="{Binding}" /> <ToggleSwitch IsChecked="{Binding IsEnabled}"
<Button Grid.Column="2" Command="{Binding #Root.DataContext.TogglePluginCommand}"
Command="{Binding #Root.DataContext.DeletePluginCommand}" CommandParameter="{Binding}" />
CommandParameter="{Binding}" <Button Command="{Binding #Root.DataContext.DeletePluginCommand}"
Content="{Binding #Root.DataContext.DeleteButtonText}" /> CommandParameter="{Binding}"
</Grid> Content="{Binding #Root.DataContext.DeleteButtonText}" />
</Border> </StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<TextBlock Classes="settings-subsection-title" <Separator Classes="settings-separator" />
Text="{Binding MarketplaceHeader}" />
<!-- 市场插件分组 -->
<controls:IconText Icon="ShoppingBag"
Text="{Binding MarketplaceHeader}"
Margin="0,0,0,4" />
<ItemsControl ItemsSource="{Binding MarketPlugins}"> <ItemsControl ItemsSource="{Binding MarketPlugins}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:PluginMarketItemViewModel"> <DataTemplate x:DataType="vm:PluginMarketItemViewModel">
<Border Classes="settings-list-item"> <ui:SettingsExpander>
<Grid Classes="settings-list-actions" <ui:SettingsExpander.IconSource>
ColumnDefinitions="*,Auto"> <fi:SymbolIconSource Symbol="ShoppingBag" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Header>
<StackPanel> <StackPanel>
<TextBlock FontWeight="SemiBold" <TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
Text="{Binding Name}" /> <TextBlock Opacity="0.76" FontSize="12" Text="{Binding Description}" />
<TextBlock Opacity="0.76"
FontSize="12"
Text="{Binding Description}" />
<TextBlock Opacity="0.58"
FontSize="11"
Text="{Binding Version}" />
</StackPanel> </StackPanel>
<Button Grid.Column="1" </ui:SettingsExpander.Header>
Command="{Binding #Root.DataContext.InstallPluginCommand}" <ui:SettingsExpander.Footer>
<Button Command="{Binding #Root.DataContext.InstallPluginCommand}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Content="{Binding #Root.DataContext.InstallButtonText}" /> Content="{Binding #Root.DataContext.InstallButtonText}" />
</Grid> </ui:SettingsExpander.Footer>
</Border> </ui:SettingsExpander>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View File

@@ -0,0 +1,51 @@
<UserControl xmlns="https://github.com/avaloniaui"
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.WallpaperSettingsPage"
x:DataType="vm:WallpaperSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container">
<controls:IconText Icon="Image"
Text="{Binding WallpaperHeader}"
Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding WallpaperPathLabel}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Image" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBox IsReadOnly="True"
MinWidth="200"
Text="{Binding WallpaperPath}" />
<Button Click="OnBrowseWallpaperClick"
Content="{Binding ImportWallpaperButtonText}" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Header="{Binding WallpaperPlacementLabel}"
Description="{Binding WallpaperPlacementDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Maximize" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox MinWidth="160"
ItemsSource="{Binding WallpaperPlacements}"
SelectedItem="{Binding SelectedWallpaperPlacement}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

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

View File

@@ -7,8 +7,8 @@
x:DataType="vm:SettingsWindowViewModel" x:DataType="vm:SettingsWindowViewModel"
Width="1120" Width="1120"
Height="760" Height="760"
MinWidth="920" MinWidth="560"
MinHeight="620" MinHeight="480"
CanResize="True" CanResize="True"
WindowStartupLocation="Manual" WindowStartupLocation="Manual"
SystemDecorations="BorderOnly" SystemDecorations="BorderOnly"
@@ -17,7 +17,29 @@
Icon="/Assets/avalonia-logo.ico" Icon="/Assets/avalonia-logo.ico"
Title="{Binding Title}"> Title="{Binding Title}">
<Grid Background="{DynamicResource AdaptiveSurfaceBaseBrush}" <Window.Resources>
<x:Double x:Key="SettingsPageContentWidth">920</x:Double>
</Window.Resources>
<Window.Styles>
<Style Selector="Grid.page-title-container">
<Setter Property="Margin" Value="0,16,0,0" />
<Setter Property="Width" Value="{DynamicResource SettingsPageContentWidth}" />
<Setter Property="HorizontalAlignment" Value="Center" />
</Style>
<Style Selector="TextBlock.page-title-text">
<Setter Property="FontSize" Value="28" />
<Setter Property="FontWeight" Value="Normal" />
</Style>
<Style Selector="TextBlock.page-title-text:narrow">
<Setter Property="FontSize" Value="24" />
</Style>
</Window.Styles>
<Grid Classes="settings-scope"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
RowDefinitions="Auto,Auto,*"> RowDefinitions="Auto,Auto,*">
<Border x:Name="WindowTitleBarHost" <Border x:Name="WindowTitleBarHost"
Height="48" Height="48"
@@ -121,10 +143,24 @@
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" /> <SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
</ui:NavigationView.Resources> </ui:NavigationView.Resources>
<Grid ColumnDefinitions="*,Auto" <Grid x:Name="SettingsContentGrid"
ColumnDefinitions="*,Auto"
ColumnSpacing="20" ColumnSpacing="20"
Margin="12,0,16,16"> Margin="12,0,16,16">
<ui:Frame x:Name="ContentFrame" /> <Grid Grid.Column="0"
RowDefinitions="Auto,*">
<Grid x:Name="PageTitleContainer"
Grid.Row="0"
Classes="page-title-container"
IsVisible="{Binding IsPageTitleVisible}">
<TextBlock x:Name="PageTitleTextBlock"
Classes="page-title-text"
Text="{Binding CurrentPageTitle}" />
</Grid>
<ui:Frame x:Name="ContentFrame"
Grid.Row="1" />
</Grid>
<Border x:Name="DrawerBorder" <Border x:Name="DrawerBorder"
Grid.Column="1" Grid.Column="1"

View File

@@ -6,6 +6,7 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
@@ -17,10 +18,17 @@ namespace LanMountainDesktop.Views;
public partial class SettingsWindow : Window, ISettingsPageHostContext public partial class SettingsWindow : Window, ISettingsPageHostContext
{ {
private const double BaseSettingsContentWidth = 960d;
private const double MinSettingsContentWidth = 320d;
private const double MaxSettingsContentWidth = 1280d;
private const double BaseDrawerWidth = 296d;
private const double BaseNarrowThreshold = 800d;
private readonly ISettingsPageRegistry _pageRegistry; private readonly ISettingsPageRegistry _pageRegistry;
private readonly IHostApplicationLifecycle _hostApplicationLifecycle; private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
private readonly Dictionary<string, Control> _cachedPages = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, Control> _cachedPages = new(StringComparer.OrdinalIgnoreCase);
private readonly bool _useSystemChrome; private readonly bool _useSystemChrome;
private bool _isResponsiveLayoutRefreshPending;
public SettingsWindow() public SettingsWindow()
: this( : this(
@@ -44,6 +52,11 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
InitializeComponent(); InitializeComponent();
ApplyChromeMode(useSystemChrome); ApplyChromeMode(useSystemChrome);
if (RootNavigationView is not null)
{
RootNavigationView.PropertyChanged += OnRootNavigationViewPropertyChanged;
}
Opened += OnOpened; Opened += OnOpened;
SizeChanged += OnWindowSizeChanged; SizeChanged += OnWindowSizeChanged;
Closed += OnClosed; Closed += OnClosed;
@@ -59,6 +72,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
SyncTitleText(); SyncTitleText();
UpdateChromeMetrics(); UpdateChromeMetrics();
UpdatePaneToggleIcon(); UpdatePaneToggleIcon();
UpdateResponsiveLayout();
RequestResponsiveLayoutRefresh();
} }
public void ReloadPages(string? pageId) public void ReloadPages(string? pageId)
@@ -86,6 +101,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
ViewModel.DrawerTitle = title ?? ViewModel.DrawerFallbackTitle; ViewModel.DrawerTitle = title ?? ViewModel.DrawerFallbackTitle;
ViewModel.IsDrawerOpen = true; ViewModel.IsDrawerOpen = true;
SyncTitleText(); SyncTitleText();
UpdateResponsiveLayout();
RequestResponsiveLayoutRefresh();
} }
public void CloseDrawer() public void CloseDrawer()
@@ -98,6 +115,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
ViewModel.IsDrawerOpen = false; ViewModel.IsDrawerOpen = false;
ViewModel.DrawerTitle = null; ViewModel.DrawerTitle = null;
SyncTitleText(); SyncTitleText();
UpdateResponsiveLayout();
RequestResponsiveLayoutRefresh();
} }
public void RequestRestart(string? reason = null) public void RequestRestart(string? reason = null)
@@ -285,6 +304,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
_ = sender; _ = sender;
_ = e; _ = e;
UpdateChromeMetrics(); UpdateChromeMetrics();
UpdateResponsiveLayout();
RequestResponsiveLayoutRefresh();
} }
private void OnWindowSizeChanged(object? sender, SizeChangedEventArgs e) private void OnWindowSizeChanged(object? sender, SizeChangedEventArgs e)
@@ -292,12 +313,139 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
_ = sender; _ = sender;
_ = e; _ = e;
UpdateChromeMetrics(); 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) private void OnClosed(object? sender, EventArgs e)
{ {
_cachedPages.Clear(); _cachedPages.Clear();
PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged; PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged;
if (RootNavigationView is not null)
{
RootNavigationView.PropertyChanged -= OnRootNavigationViewPropertyChanged;
}
Opened -= OnOpened; Opened -= OnOpened;
SizeChanged -= OnWindowSizeChanged; SizeChanged -= OnWindowSizeChanged;
} }
@@ -322,6 +470,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
RootNavigationView.IsPaneOpen = !RootNavigationView.IsPaneOpen; RootNavigationView.IsPaneOpen = !RootNavigationView.IsPaneOpen;
UpdatePaneToggleIcon(); UpdatePaneToggleIcon();
UpdateResponsiveLayout();
RequestResponsiveLayoutRefresh();
} }
private void OnCloseWindowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) private void OnCloseWindowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -331,6 +481,46 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
Close(); 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() private void UpdatePaneToggleIcon()
{ {
if (TogglePaneButtonIcon is null || RootNavigationView is null) if (TogglePaneButtonIcon is null || RootNavigationView is null)
@@ -457,6 +647,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
return iconKey?.Trim() switch return iconKey?.Trim() switch
{ {
"DesignIdeas" => Symbol.Color, "DesignIdeas" => Symbol.Color,
"Image" => Symbol.Image,
"GridDots" => Symbol.GridDots, "GridDots" => Symbol.GridDots,
"PuzzlePiece" => Symbol.PuzzlePiece, "PuzzlePiece" => Symbol.PuzzlePiece,
"Info" => Symbol.Info, "Info" => Symbol.Info,