settings_re4

This commit is contained in:
lincube
2026-03-13 22:20:12 +08:00
parent 3b3f060f33
commit 5fdaa2539b
89 changed files with 5778 additions and 192 deletions

View File

@@ -0,0 +1,32 @@
# Checklist - 设置页面 Fluent 设计改造
## Phase 1: 分析与准备
- [ ] SettingsExpander 控件分析完成
- [ ] 当前布局问题定位完成
## Phase 2: 窗口布局调整
- [ ] SettingsWindow 内容区域无额外 Border 包裹
- [ ] 窗口整体视觉效果正常
- [ ] 窗口圆角在不同模式下正确显示
## Phase 3: 设置页面改造
- [ ] AppearanceSettingsPage 无额外边框包裹
- [ ] GeneralSettingsPage 无额外边框包裹
- [ ] ComponentsSettingsPage 无额外边框包裹
- [ ] PluginsSettingsPage 无额外边框包裹
- [ ] AboutSettingsPage 无额外边框包裹
## Phase 4: 视觉规范
- [ ] 设置项间距统一
- [ ] 圆角样式统一
- [ ] 页面标题样式统一
## 验证
- [ ] 编译通过,无错误
- [ ] 运行正常,设置页面可正常显示
- [ ] 视觉效果符合 Fluent 设计风格

View File

@@ -0,0 +1,76 @@
# 设置页面 Fluent 设计改造规格说明书
## Why
当前 LanMountainDesktop 设置页面存在以下问题:
1. 右侧详细设置区域被额外边框包裹,未能实现 Fluent Avalonia 控件的完整填充效果
2. 设置项未采用 Fluent 卡片设计风格,仍使用传统 Border + StackPanel 布局
3. 与 ClassIsland 项目的视觉风格差异较大
## What Changes
- 移除页面内容区域的额外 Border 包裹,直接使用 ScrollViewer + StackPanel
- 参考 ClassIsland 项目,引入 SettingsExpander 控件替代传统布局
- 统一设置项的间距、圆角、字体等视觉规范
- 修改窗口布局,移除内容区域的 glass-panel 样式
## Impact
### Affected specs
- 设置页面 UI 布局规范
- Fluent 设计风格适配
### Affected code
- `Views/SettingsPages/*.axaml` - 所有设置页面
- `Views/SettingsWindow.axaml` - 设置窗口布局
- `Styles/GlassModule.axaml` - 样式资源
---
## ADDED Requirements
### Requirement: 设置页面 Fluent 卡片设计
系统 SHALL 提供类似 ClassIsland 的 SettingsExpander 卡片式设置项。
#### Scenario: 设置页面布局
- **WHEN** 用户打开任意设置页面
- **THEN** 页面使用 ScrollViewer 直接包裹内容,无额外 Border 包裹
- **AND THEN** 设置项使用 SettingsExpander 或 Fluent 卡片样式
### Requirement: 移除内容区域额外边框
系统 SHALL 移除右侧内容区域的 glass-panel 边框包裹。
#### Scenario: 内容区域无额外边框
- **WHEN** 用户查看设置页面内容
- **THEN** 内容直接显示在透明背景上,无额外边框包裹
### Requirement: 设置项视觉规范
系统 SHALL 统一设置项的视觉样式。
#### Scenario: 设置项样式
- **WHEN** 开发者创建新的设置项
- **THEN** 使用统一的间距Spacing、圆角、字体大小
- **AND THEN** 参考 ClassIsland 的 SettingsExpander 样式
---
## MODIFIED Requirements
### Requirement: 设置页面布局结构
**当前**: Border → ScrollViewer → Border → StackPanel → 内容
**修改后**: ScrollViewer → StackPanel → 设置项(无额外 Border
---
## REMOVED Requirements
### Requirement: 传统 Border 包裹布局
**Reason**: 实现 Fluent 设计风格,移除视觉噪音
**Migration**: 将现有 Border 包裹改为直接内容布局

View File

@@ -0,0 +1,51 @@
# Tasks - 设置页面 Fluent 设计改造
## Phase 1: 分析与准备
- [ ] Task 1.1: 分析 ClassIsland SettingsExpander 控件实现
- [ ] 查看 ClassIsland.Core 中的 SettingsExpander 定义
- [ ] 分析样式模板和视觉效果
- [ ] 确定是否需要自定义控件或使用现有替代方案
- [ ] Task 1.2: 分析当前设置页面布局问题
- [ ] 定位右侧内容区域的 Border 包裹代码
- [ ] 分析 glass-panel 样式对布局的影响
## Phase 2: 窗口布局调整
- [ ] Task 2.1: 修改 SettingsWindow.axaml 内容区域布局
- [ ] 移除 Frame 外部的 glass-panel Border
- [ ] 直接使用透明背景
- [ ] 验证窗口整体视觉效果
## Phase 3: 设置页面改造
- [ ] Task 3.1: 改造 AppearanceSettingsPage 页面
- [ ] 移除外部的 glass-panel Border
- [ ] 调整内容布局为直接填充
- [ ] 验证视觉效果
- [ ] Task 3.2: 改造 GeneralSettingsPage 页面
- [ ] 移除外部的 glass-panel Border
- [ ] 调整内容布局
- [ ] Task 3.3: 改造其他设置页面
- [ ] ComponentsSettingsPage
- [ ] PluginsSettingsPage
- [ ] AboutSettingsPage
## Phase 4: 视觉规范统一
- [ ] Task 4.1: 统一设置项间距和圆角
- [ ] 定义统一的 Spacing 值
- [ ] 统一圆角大小
- [ ] Task 4.2: 优化页面标题区域样式
- [ ] 调整 Page Header 字体大小
- [ ] 优化 Description 样式
## Task Dependencies
- Task 1.2 依赖 Task 1.1
- Task 2.1 依赖 Task 1.2
- Task 3.x 依赖 Task 2.1
- Task 4.x 依赖 Task 3.x

View File

@@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace LanMountainDesktop.PluginSdk;
public interface ISettingsPageHostContext
{
void OpenDrawer(Control content, string? title = null);
void CloseDrawer();
void RequestRestart(string? reason = null);
}

View File

@@ -85,7 +85,10 @@ public sealed record PluginManifest(
if (requestedVersion.Major != currentVersion.Major)
{
throw new InvalidOperationException(
$"Plugin '{normalized.Id}' targets API version '{normalized.ApiVersion}', but the host provides '{PluginSdkInfo.ApiVersion}'. Upgrade the plugin to API {PluginSdkInfo.ApiVersion}.");
$"Plugin '{normalized.Id}' targets API version '{normalized.ApiVersion}' (major {requestedVersion.Major}), " +
$"but the host provides '{PluginSdkInfo.ApiVersion}' (major {currentVersion.Major}). " +
$"This host only supports v{currentVersion.Major}.x plugins. " +
$"Migrate the plugin to API {PluginSdkInfo.ApiVersion} and rebuild the package.");
}
return normalized;

View File

@@ -2,7 +2,7 @@ namespace LanMountainDesktop.PluginSdk;
public static class PluginSdkInfo
{
public const string ApiVersion = "2.0.0";
public const string ApiVersion = "3.0.0";
public const string ManifestFileName = "plugin.json";
public const string PackageFileExtension = ".laapp";
public const string DataDirectoryName = "Data";

View File

@@ -0,0 +1,54 @@
using System;
using Avalonia.Controls;
namespace LanMountainDesktop.PluginSdk;
public abstract class SettingsPageBase : UserControl
{
public static readonly string DialogHostIdentifier = "LanMountainDesktop.SettingsWindow";
private ISettingsPageHostContext? _hostContext;
public ISettingsPageHostContext? HostContext => _hostContext;
public Uri? NavigationUri { get; set; }
public void InitializeHostContext(ISettingsPageHostContext hostContext)
{
_hostContext = hostContext;
}
public virtual void OnNavigatedTo(object? parameter)
{
}
protected void OpenDrawer(Control content, string? title = null)
{
_hostContext?.OpenDrawer(content, title);
}
protected void OpenDrawer(object content, bool usePageDataContext = false, object? dataContext = null, string? title = null)
{
if (content is Control control && !usePageDataContext)
{
control.DataContext = dataContext ?? DataContext ?? this;
OpenDrawer(control, title);
return;
}
if (content is Control drawerControl)
{
OpenDrawer(drawerControl, title);
}
}
protected void CloseDrawer()
{
_hostContext?.CloseDrawer();
}
protected void RequestRestart(string? reason = null)
{
_hostContext?.RequestRestart(reason);
}
}

View File

@@ -0,0 +1,10 @@
namespace LanMountainDesktop.PluginSdk;
public enum SettingsPageCategory
{
General = 0,
Appearance = 10,
Components = 20,
Plugins = 30,
About = 40
}

View File

@@ -0,0 +1,46 @@
using System;
namespace LanMountainDesktop.PluginSdk;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class SettingsPageInfoAttribute : Attribute
{
public SettingsPageInfoAttribute(
string id,
string name,
SettingsPageCategory category)
{
ArgumentException.ThrowIfNullOrWhiteSpace(id);
ArgumentException.ThrowIfNullOrWhiteSpace(name);
Id = id.Trim();
Name = name.Trim();
Category = category;
}
public string Id { get; }
public string Name { get; }
public SettingsPageCategory Category { get; }
public string? TitleLocalizationKey { get; init; }
public string? DescriptionLocalizationKey { get; init; }
public string IconKey { get; init; } = "Settings";
public string? SelectedIconKey { get; init; }
public int SortOrder { get; init; }
public bool HideDefault { get; init; }
public bool HidePageTitle { get; init; }
public bool UseFullWidth { get; init; }
public string? GroupId { get; init; }
public SettingsScope Scope { get; init; } = SettingsScope.App;
}

View File

@@ -1,4 +1,4 @@
<Application xmlns="https://github.com/avaloniaui"
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sty="using:FluentAvalonia.Styling"
xmlns:fi="using:FluentIcons.Avalonia"
@@ -22,6 +22,7 @@
<StyleInclude Source="avares://LanMountainDesktop/Styles/FluttermotionToken.axaml" />
<StyleInclude Source="avares://LanMountainDesktop/Styles/GlassModule.axaml" />
<StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsAnimations.axaml" />
<StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsCardStyles.axaml" />
<Style Selector="Window">
<Setter Property="FontFamily" Value="{DynamicResource AppFontFamily}" />

View File

@@ -1,5 +1,7 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
@@ -7,6 +9,7 @@ using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
using AvaloniaWebView;
using LanMountainDesktop.ComponentSystem;
@@ -38,6 +41,9 @@ public partial class App : Application
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService();
private ISettingsPageRegistry? _settingsPageRegistry;
private ISettingsWindowService? _settingsWindowService;
private bool _exitCleanupCompleted;
private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop;
private ShutdownIntent _shutdownIntent;
@@ -46,6 +52,7 @@ public partial class App : Application
private PluginRuntimeService? _pluginRuntimeService;
private MainWindow? _mainWindow;
private bool _mainWindowClosed;
private bool _uiUnhandledExceptionHooked;
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
@@ -54,12 +61,18 @@ public partial class App : Application
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
public ISettingsFacadeService SettingsFacade => _settingsFacade;
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
internal ISettingsWindowService? SettingsWindowService => _settingsWindowService;
internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
{
EnsureSettingsWindowService();
AppLogger.Info(
"SettingsFacade",
$"Settings UI entry is disabled by hard-cut migration. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
$"Opening settings window. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
_settingsWindowService?.Open(new SettingsWindowOpenRequest(
Source: source,
Owner: _mainWindow is { IsVisible: true } ? _mainWindow : null,
PageId: pageTag));
}
public override void Initialize()
@@ -68,11 +81,15 @@ public partial class App : Application
ConfigureWebViewUserDataFolder();
AvaloniaWebViewBuilder.Initialize(default);
AvaloniaXamlLoader.Load(this);
ApplyInitialThemeVariantFromSettings();
ApplyCurrentCultureFromSettings();
EnsureSettingsWindowService();
}
public override void OnFrameworkInitializationCompleted()
{
AppLogger.Info("App", "Framework initialization completed.");
RegisterUiUnhandledExceptionGuard();
LinuxDesktopEntryInstaller.EnsureInstalled();
InitializePluginRuntime();
AppSettingsService.SettingsSaved += OnAppSettingsSaved;
@@ -116,6 +133,25 @@ public partial class App : Application
Reason: "User selected Restart App from the tray menu."));
}
private void OnTraySettingsClick(object? sender, EventArgs e)
{
_ = sender;
_ = e;
OpenIndependentSettingsModule("TrayMenu");
}
private void OnTrayComponentLibraryClick(object? sender, EventArgs e)
{
_ = sender;
_ = e;
if (_mainWindow is null)
{
return;
}
_detachedComponentLibraryWindowService.Open(_mainWindow);
}
private void DisableAvaloniaDataAnnotationValidation()
{
// Get an array of plugins to remove
@@ -204,6 +240,14 @@ public partial class App : Application
showDesktopItem.Click += OnTrayShowDesktopClick;
menu.Items.Add(showDesktopItem);
var settingsItem = new NativeMenuItem(L("tray.menu.settings", "Settings"));
settingsItem.Click += OnTraySettingsClick;
menu.Items.Add(settingsItem);
var componentLibraryItem = new NativeMenuItem(L("tray.menu.component_library", "Component Library"));
componentLibraryItem.Click += OnTrayComponentLibraryClick;
menu.Items.Add(componentLibraryItem);
menu.Items.Add(new NativeMenuItemSeparator());
var restartItem = new NativeMenuItem(L("tray.menu.restart", "Restart App"));
@@ -235,6 +279,48 @@ public partial class App : Application
_trayIcons = null;
}
private void EnsureSettingsWindowService()
{
_settingsPageRegistry ??= new SettingsPageRegistry(
_settingsFacade,
_hostApplicationLifecycle,
_localizationService,
() => _pluginRuntimeService);
_settingsWindowService ??= new SettingsWindowService(
_settingsPageRegistry,
_hostApplicationLifecycle,
_settingsFacade);
}
private void ApplyInitialThemeVariantFromSettings()
{
var themeState = _settingsFacade.Theme.Get();
RequestedThemeVariant = themeState.IsNightMode
? ThemeVariant.Dark
: ThemeVariant.Light;
}
private void ApplyCurrentCultureFromSettings()
{
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
CultureInfo culture;
try
{
culture = CultureInfo.GetCultureInfo(languageCode);
}
catch (CultureNotFoundException)
{
culture = CultureInfo.GetCultureInfo("zh-CN");
}
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
}
private void ActivateMainWindow()
{
RestoreOrCreateMainWindow(showSingleInstanceNotice: true, source: "SingleInstance");
@@ -338,6 +424,8 @@ public partial class App : Application
{
Dispatcher.UIThread.Post(() =>
{
ApplyInitialThemeVariantFromSettings();
ApplyCurrentCultureFromSettings();
if (_trayIcons is not null)
{
InitializeTrayIcon();
@@ -345,6 +433,43 @@ public partial class App : Application
}, DispatcherPriority.Background);
}
private void RegisterUiUnhandledExceptionGuard()
{
if (_uiUnhandledExceptionHooked)
{
return;
}
Dispatcher.UIThread.UnhandledException += OnUiThreadUnhandledException;
_uiUnhandledExceptionHooked = true;
}
private void OnUiThreadUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e)
{
if (!IsKnownWebViewStartupException(e.Exception))
{
return;
}
e.Handled = true;
AppLogger.Warn(
"WebView2",
"Suppressed a known WebView startup exception from AvaloniaWebView.Navigate to keep the host process alive.",
e.Exception);
}
private static bool IsKnownWebViewStartupException(Exception exception)
{
if (exception is not NullReferenceException)
{
return false;
}
var stackTrace = exception.StackTrace ?? string.Empty;
return stackTrace.Contains("AvaloniaWebView.WebView.Navigate", StringComparison.Ordinal) &&
stackTrace.Contains("AvaloniaWebView.WebView.OnAttachedToVisualTree", StringComparison.Ordinal);
}
private void PerformExitCleanup()
{
if (_exitCleanupCompleted)
@@ -368,6 +493,12 @@ public partial class App : Application
_pluginRuntimeService = null;
}
_settingsWindowService?.Close();
if (_settingsPageRegistry is IDisposable disposableRegistry)
{
disposableRegistry.Dispose();
}
AudioRecorderServiceFactory.DisposeSharedServices();
StudyAnalyticsServiceFactory.DisposeSharedService();
DisposeTrayIcon();

View File

@@ -1,9 +1,13 @@
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.ComponentSystem;
public sealed record DesktopComponentRuntimeContext(
string ComponentId,
string? PlacementId,
ISettingsFacadeService SettingsFacade,
ISettingsService SettingsService,
IComponentSettingsAccessor ComponentSettingsAccessor);
IComponentSettingsAccessor ComponentSettingsAccessor,
IComponentInstanceSettingsStore ComponentSettingsStore);

View File

@@ -0,0 +1,18 @@
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.ComponentSystem;
public sealed record DesktopComponentSettingsContext(
string ComponentId,
string? PlacementId,
ISettingsFacadeService SettingsFacade,
ISettingsService SettingsService,
IComponentSettingsAccessor ComponentSettingsAccessor,
IComponentInstanceSettingsStore ComponentSettingsStore);
public interface IComponentSettingsContextAware
{
void SetComponentSettingsContext(DesktopComponentSettingsContext context);
}

View File

@@ -0,0 +1,37 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:LanMountainDesktop.Controls"
xmlns:fi="using:FluentIcons.Avalonia"
x:Class="LanMountainDesktop.Controls.SettingsOptionCard"
x:Name="Root">
<Border Classes="settings-option-card">
<Grid RowDefinitions="Auto,Auto"
RowSpacing="12">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="14">
<Border x:Name="IconHost"
Classes="settings-option-card-icon-host">
<fi:SymbolIcon x:Name="CardIcon"
Classes="icon-m" />
</Border>
<StackPanel Grid.Column="1"
Spacing="4"
VerticalAlignment="Center">
<TextBlock x:Name="TitleTextBlock"
Classes="settings-item-label" />
<TextBlock x:Name="DescriptionTextBlock"
Classes="settings-item-description" />
</StackPanel>
<ContentPresenter x:Name="ActionContentHost"
Grid.Column="2"
VerticalAlignment="Center" />
</Grid>
<ContentPresenter x:Name="DetailsContentHost"
Grid.Row="1"
Margin="54,0,0,0" />
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,112 @@
using Avalonia;
using Avalonia.Controls;
using FluentIcons.Common;
namespace LanMountainDesktop.Controls;
public partial class SettingsOptionCard : UserControl
{
public static readonly StyledProperty<string?> IconKeyProperty =
AvaloniaProperty.Register<SettingsOptionCard, string?>(nameof(IconKey), "Settings");
public static readonly StyledProperty<string?> TitleProperty =
AvaloniaProperty.Register<SettingsOptionCard, string?>(nameof(Title));
public static readonly StyledProperty<string?> DescriptionProperty =
AvaloniaProperty.Register<SettingsOptionCard, string?>(nameof(Description));
public static readonly StyledProperty<object?> ActionContentProperty =
AvaloniaProperty.Register<SettingsOptionCard, object?>(nameof(ActionContent));
public static readonly StyledProperty<object?> DetailsContentProperty =
AvaloniaProperty.Register<SettingsOptionCard, object?>(nameof(DetailsContent));
public SettingsOptionCard()
{
InitializeComponent();
RefreshVisualState();
}
public string? IconKey
{
get => GetValue(IconKeyProperty);
set => SetValue(IconKeyProperty, value);
}
public string? Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public string? Description
{
get => GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
public object? ActionContent
{
get => GetValue(ActionContentProperty);
set => SetValue(ActionContentProperty, value);
}
public object? DetailsContent
{
get => GetValue(DetailsContentProperty);
set => SetValue(DetailsContentProperty, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IconKeyProperty ||
change.Property == TitleProperty ||
change.Property == DescriptionProperty ||
change.Property == ActionContentProperty ||
change.Property == DetailsContentProperty)
{
RefreshVisualState();
}
}
private void RefreshVisualState()
{
if (CardIcon is null ||
IconHost is null ||
TitleTextBlock is null ||
DescriptionTextBlock is null ||
ActionContentHost is null ||
DetailsContentHost is null)
{
return;
}
CardIcon.Symbol = MapIcon(IconKey);
IconHost.IsVisible = !string.IsNullOrWhiteSpace(IconKey);
TitleTextBlock.Text = Title ?? string.Empty;
DescriptionTextBlock.Text = Description ?? string.Empty;
DescriptionTextBlock.IsVisible = !string.IsNullOrWhiteSpace(Description);
ActionContentHost.Content = ActionContent;
ActionContentHost.IsVisible = ActionContent is not null;
DetailsContentHost.Content = DetailsContent;
DetailsContentHost.IsVisible = DetailsContent is not null;
}
private static Symbol MapIcon(string? iconKey)
{
return iconKey?.Trim() switch
{
"DesignIdeas" => Symbol.Color,
"GridDots" => Symbol.GridDots,
"PuzzlePiece" => Symbol.PuzzlePiece,
"Info" => Symbol.Info,
"ArrowSync" => Symbol.ArrowSync,
_ => Symbol.Settings
};
}
}

View File

@@ -0,0 +1,33 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:LanMountainDesktop.Controls"
xmlns:fi="using:FluentIcons.Avalonia"
x:Class="LanMountainDesktop.Controls.SettingsSectionCard"
x:Name="Root">
<Border Classes="settings-section-card">
<Grid RowDefinitions="Auto,Auto"
RowSpacing="16">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="14">
<Border x:Name="IconHost"
Classes="settings-section-card-icon-host">
<fi:SymbolIcon x:Name="CardIcon"
Classes="icon-l" />
</Border>
<StackPanel Grid.Column="1"
Spacing="4">
<TextBlock x:Name="TitleTextBlock"
Classes="settings-card-header"
Margin="0" />
<TextBlock x:Name="DescriptionTextBlock"
Classes="settings-card-description"
Margin="0" />
</StackPanel>
</Grid>
<ContentPresenter x:Name="CardContentHost"
Grid.Row="1" />
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,97 @@
using Avalonia;
using Avalonia.Controls;
using FluentIcons.Common;
namespace LanMountainDesktop.Controls;
public partial class SettingsSectionCard : UserControl
{
public static readonly StyledProperty<string?> IconKeyProperty =
AvaloniaProperty.Register<SettingsSectionCard, string?>(nameof(IconKey), "Settings");
public static readonly StyledProperty<string?> TitleProperty =
AvaloniaProperty.Register<SettingsSectionCard, string?>(nameof(Title));
public static readonly StyledProperty<string?> DescriptionProperty =
AvaloniaProperty.Register<SettingsSectionCard, string?>(nameof(Description));
public static readonly StyledProperty<object?> CardContentProperty =
AvaloniaProperty.Register<SettingsSectionCard, object?>(nameof(CardContent));
public SettingsSectionCard()
{
InitializeComponent();
RefreshVisualState();
}
public string? IconKey
{
get => GetValue(IconKeyProperty);
set => SetValue(IconKeyProperty, value);
}
public string? Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public string? Description
{
get => GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
public object? CardContent
{
get => GetValue(CardContentProperty);
set => SetValue(CardContentProperty, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IconKeyProperty ||
change.Property == TitleProperty ||
change.Property == DescriptionProperty ||
change.Property == CardContentProperty)
{
RefreshVisualState();
}
}
private void RefreshVisualState()
{
if (CardIcon is null ||
IconHost is null ||
TitleTextBlock is null ||
DescriptionTextBlock is null ||
CardContentHost is null)
{
return;
}
CardIcon.Symbol = MapIcon(IconKey);
IconHost.IsVisible = !string.IsNullOrWhiteSpace(IconKey);
TitleTextBlock.Text = Title ?? string.Empty;
DescriptionTextBlock.Text = Description ?? string.Empty;
DescriptionTextBlock.IsVisible = !string.IsNullOrWhiteSpace(Description);
CardContentHost.Content = CardContent;
}
private static Symbol MapIcon(string? iconKey)
{
return iconKey?.Trim() switch
{
"DesignIdeas" => Symbol.Color,
"GridDots" => Symbol.GridDots,
"PuzzlePiece" => Symbol.PuzzlePiece,
"Info" => Symbol.Info,
"ArrowSync" => Symbol.ArrowSync,
_ => Symbol.Settings
};
}
}

View File

@@ -1,8 +1,9 @@
{
{
"app.title": "LanMountainDesktop",
"tray.tooltip": "LanMountainDesktop",
"tray.menu.show_desktop": "Open Desktop",
"tray.menu.settings": "Settings",
"tray.menu.component_library": "Component Library",
"tray.menu.restart": "Restart App",
"tray.menu.exit": "Exit App",
"button.back_to_windows": "Back to Windows",
@@ -221,6 +222,32 @@
"settings.region.timezone_header": "Time Zone",
"settings.region.timezone_desc": "Select a time zone. Clock and calendar widgets will follow this zone.",
"settings.region.applied_format": "Language switched to: {0}",
"settings.region.follow_system": "Follow system default",
"settings.general.title": "General",
"settings.general.description": "Adjust language, time zone, and runtime behavior.",
"settings.general.basic_header": "Basic Settings",
"settings.general.runtime_header": "Runtime",
"settings.general.preview_header": "Date & Time Preview",
"settings.general.preview_time_label": "Time",
"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.theme_header": "Theme",
"settings.color.enable_night_mode_toggle": "Enable night mode",
"settings.color.use_system_chrome_toggle": "Use system window chrome",
"settings.color.theme_color_label": "Theme accent color",
"settings.wallpaper.placement.fill": "Fill",
"settings.wallpaper.placement.fit": "Fit",
"settings.wallpaper.placement.stretch": "Stretch",
"settings.wallpaper.placement.center": "Center",
"settings.wallpaper.placement.tile": "Tile",
"settings.status_bar.clock_format_label": "Clock format",
"settings.status_bar.clock_format.hm": "Hour:Minute",
"settings.status_bar.clock_format.hms": "Hour:Minute:Second",
"settings.components.title": "Components",
"settings.components.description": "Adjust desktop grid density and widget placement.",
"settings.components.grid_header": "Grid Layout",
"settings.update.title": "Update",
"settings.update.current_version_label": "Current Version",
"settings.update.latest_version_label": "Latest Release",
@@ -273,6 +300,12 @@
"settings.about.render_mode.current_format": "Current backend: {0}",
"settings.about.render_mode.impl_format": "Runtime implementation: {0}",
"settings.about.render_mode.impl_unavailable": "Runtime implementation details are unavailable.",
"settings.about.description": "Application details and update preferences.",
"settings.about.app_info_header": "Application Information",
"settings.about.update_header": "Updates",
"settings.about.version_label": "Version",
"settings.about.render_backend_label": "Render Backend",
"settings.about.render_backend_format": "Render Backend: {0}",
"settings.restart_dialog.title": "Restart required",
"settings.restart_dialog.render_mode_message": "Restart the app to switch the rendering mode from \"{0}\" to \"{1}\". Restart now?",
"settings.restart_dialog.restart": "Restart now",
@@ -314,6 +347,19 @@
"settings.plugins.runtime_desc": "Review plugin runtime state and load results.",
"settings.plugins.runtime_hint": "This page shows discovery status, load results, and runtime diagnostics for installed plugins.",
"settings.plugins.runtime_status": "Plugin runtime status will appear here after plugin discovery completes.",
"settings.plugins.description": "Manage installed plugins and browse marketplace packages.",
"settings.plugins.initial_status": "Refresh plugin state to see the latest installed and marketplace entries.",
"settings.plugins.refresh_button": "Refresh Plugins",
"settings.plugins.refresh_success_format": "Loaded {0} installed plugins and {1} marketplace entries.",
"settings.plugins.refresh_failed": "Failed to load plugin market index.",
"settings.plugins.marketplace_header": "Marketplace",
"settings.plugins.marketplace_empty": "No marketplace plugins are available right now.",
"settings.plugins.delete_button_short": "Delete",
"settings.plugins.install_button_short": "Install",
"settings.plugins.restart_required": "Plugin changes take effect after restart.",
"settings.plugins.toggle_unchanged_format": "Plugin '{0}' did not change.",
"settings.plugins.delete_failed_name_format": "Failed to remove plugin '{0}'.",
"settings.plugins.install_failed_name_format": "Failed to install '{0}'.",
"settings.plugins.installed_header": "Installed Plugins",
"settings.plugins.installed_desc": "Review installed plugins and remove them here.",
"settings.plugins.import_header": "Install From Package",
@@ -357,6 +403,12 @@
"settings.plugin_market.title": "Plugin Market",
"settings.plugin_market.subtitle": "Browse plugins from the official LanAirApp source and stage installs.",
"settings.plugin_market.unavailable": "Plugin runtime is not available, so the official market cannot be opened right now.",
"settings.update.status_idle": "No update check has been performed yet.",
"settings.update.status_preferences_saved": "Update preferences saved.",
"settings.update.status_check_failed": "Failed to check for updates.",
"settings.update.status_available_summary_format": "Update available: {0} (current: {1})",
"settings.update.status_up_to_date_format": "You are up to date ({0}).",
"settings.window.drawer_default": "Details",
"market.toolbar.search_placeholder": "Search plugins",
"market.toolbar.refresh": "Refresh",
"market.status.loading": "Loading the official plugin market...",

View File

@@ -1,8 +1,9 @@
{
{
"app.title": "LanMountainDesktop",
"tray.tooltip": "LanMountainDesktop",
"tray.menu.show_desktop": "打开桌面",
"tray.menu.settings": "设置",
"tray.menu.component_library": "独立组件库",
"tray.menu.restart": "重启应用",
"tray.menu.exit": "退出应用",
"button.back_to_windows": "回到Windows",
@@ -221,6 +222,32 @@
"settings.region.timezone_header": "时区",
"settings.region.timezone_desc": "选择时区。时钟与日历组件会使用该时区。",
"settings.region.applied_format": "语言已切换为:{0}",
"settings.region.follow_system": "跟随系统默认",
"settings.general.title": "基本设置",
"settings.general.description": "调整语言、时区与运行时行为。",
"settings.general.basic_header": "基础设置",
"settings.general.runtime_header": "运行设置",
"settings.general.preview_header": "日期与时间预览",
"settings.general.preview_time_label": "时间",
"settings.general.preview_date_label": "日期",
"settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。",
"settings.appearance.title": "外观",
"settings.appearance.description": "切换主题、壁纸和状态栏展示。",
"settings.appearance.theme_header": "主题",
"settings.color.enable_night_mode_toggle": "启用夜间模式",
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
"settings.color.theme_color_label": "主题强调色",
"settings.wallpaper.placement.fill": "填充",
"settings.wallpaper.placement.fit": "适应",
"settings.wallpaper.placement.stretch": "拉伸",
"settings.wallpaper.placement.center": "居中",
"settings.wallpaper.placement.tile": "平铺",
"settings.status_bar.clock_format_label": "时钟格式",
"settings.status_bar.clock_format.hm": "时:分",
"settings.status_bar.clock_format.hms": "时:分:秒",
"settings.components.title": "组件",
"settings.components.description": "调整桌面网格与组件摆放密度。",
"settings.components.grid_header": "网格布局",
"settings.update.title": "更新",
"settings.update.current_version_label": "当前版本",
"settings.update.latest_version_label": "最新发布",
@@ -273,6 +300,12 @@
"settings.about.render_mode.current_format": "当前后端:{0}",
"settings.about.render_mode.impl_format": "运行时实现:{0}",
"settings.about.render_mode.impl_unavailable": "当前无法获取运行时实现信息。",
"settings.about.description": "应用信息与更新偏好。",
"settings.about.app_info_header": "应用信息",
"settings.about.update_header": "更新",
"settings.about.version_label": "版本",
"settings.about.render_backend_label": "渲染后端",
"settings.about.render_backend_format": "渲染后端:{0}",
"settings.restart_dialog.title": "需要重启应用",
"settings.restart_dialog.render_mode_message": "需要重启应用,才能将渲染模式从“{0}”切换到“{1}”。是否现在重启?",
"settings.restart_dialog.restart": "立即重启",
@@ -314,6 +347,19 @@
"settings.plugins.runtime_desc": "查看插件运行时状态、加载结果与诊断信息。",
"settings.plugins.runtime_hint": "这里展示已安装插件的发现结果、加载状态和运行时诊断信息。",
"settings.plugins.runtime_status": "插件扫描完成后,运行时状态会显示在这里。",
"settings.plugins.description": "管理已安装插件并浏览插件市场。",
"settings.plugins.initial_status": "刷新插件状态以查看最新的已安装插件和市场条目。",
"settings.plugins.refresh_button": "刷新插件",
"settings.plugins.refresh_success_format": "已加载 {0} 个已安装插件和 {1} 个市场条目。",
"settings.plugins.refresh_failed": "加载插件市场索引失败。",
"settings.plugins.marketplace_header": "插件市场",
"settings.plugins.marketplace_empty": "当前没有可用的市场插件。",
"settings.plugins.delete_button_short": "删除",
"settings.plugins.install_button_short": "安装",
"settings.plugins.restart_required": "插件变更将在重启后生效。",
"settings.plugins.toggle_unchanged_format": "插件“{0}”没有变化。",
"settings.plugins.delete_failed_name_format": "移除插件“{0}”失败。",
"settings.plugins.install_failed_name_format": "安装插件“{0}”失败。",
"settings.plugins.installed_header": "已安装插件",
"settings.plugins.installed_desc": "在这里查看和删除已安装的插件。",
"settings.plugins.import_header": "从安装包导入",
@@ -357,6 +403,12 @@
"settings.plugin_market.title": "插件市场",
"settings.plugin_market.subtitle": "浏览来自 LanAirApp 官方源的插件,并将安装暂存到本地。",
"settings.plugin_market.unavailable": "插件运行时不可用,暂时无法打开官方市场。",
"settings.update.status_idle": "尚未执行更新检查。",
"settings.update.status_preferences_saved": "更新偏好已保存。",
"settings.update.status_check_failed": "检查更新失败。",
"settings.update.status_available_summary_format": "发现更新:{0}(当前:{1})。",
"settings.update.status_up_to_date_format": "当前已是最新版本({0})。",
"settings.window.drawer_default": "详情",
"market.toolbar.search_placeholder": "搜索插件",
"market.toolbar.refresh": "刷新",
"market.status.loading": "正在加载官方插件市场...",

View File

@@ -14,6 +14,8 @@ public sealed class AppSettingsSnapshot
public string? ThemeColor { get; set; }
public bool UseSystemChrome { get; set; }
public string? WallpaperPath { get; set; }
public string WallpaperPlacement { get; set; } = "Fill";

View File

@@ -12,6 +12,8 @@ namespace LanMountainDesktop;
sealed class Program
{
internal static string StartupRenderMode { get; private set; } = AppRenderingModeHelper.Default;
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
@@ -44,6 +46,7 @@ sealed class Program
try
{
var renderMode = LoadConfiguredRenderMode();
StartupRenderMode = renderMode;
AppLogger.Info("Startup", $"Resolved render mode '{renderMode}'.");
App.CurrentSingleInstanceService = singleInstance;
BuildAvaloniaApp(renderMode).StartWithClassicDesktopLifetime(args);

View File

@@ -6,29 +6,20 @@ using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Views;
using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Services;
public sealed record ComponentLibraryCreateContext(
double CellSize,
TimeZoneService TimeZoneService,
IWeatherInfoService WeatherInfoService,
IRecommendationInfoService RecommendationInfoService,
ICalculatorDataService CalculatorDataService,
string? PlacementId = null);
public interface IComponentLibraryService
public interface IEmbeddedComponentLibraryService
{
IReadOnlyList<DesktopComponentDefinition> GetDefinitions();
void Open(MainWindow window);
bool TryCreateControl(
string componentId,
ComponentLibraryCreateContext context,
out Control? control,
out Exception? exception);
void Close(MainWindow window);
void Toggle(MainWindow window);
}
public interface IComponentLibraryWindowService
public interface IDetachedComponentLibraryWindowService
{
void Open(MainWindow window);
@@ -53,6 +44,31 @@ internal sealed class ComponentLibraryService : IComponentLibraryService
return _registry.GetAll().ToArray();
}
public IReadOnlyList<ComponentLibraryCategoryEntry> GetDesktopCategories()
{
return _runtimeRegistry
.GetDesktopComponents()
.GroupBy(
descriptor => string.IsNullOrWhiteSpace(descriptor.Definition.Category)
? "Other"
: descriptor.Definition.Category.Trim(),
StringComparer.OrdinalIgnoreCase)
.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
.Select(group => new ComponentLibraryCategoryEntry(
group.Key,
group
.OrderBy(descriptor => descriptor.Definition.DisplayName, StringComparer.OrdinalIgnoreCase)
.Select(descriptor => new ComponentLibraryComponentEntry(
descriptor.Definition.Id,
descriptor.Definition.DisplayName,
descriptor.DisplayNameLocalizationKey,
group.Key,
descriptor.Definition.MinWidthCells,
descriptor.Definition.MinHeightCells))
.ToArray()))
.ToArray();
}
public bool TryCreateControl(
string componentId,
ComponentLibraryCreateContext context,
@@ -75,6 +91,7 @@ internal sealed class ComponentLibraryService : IComponentLibraryService
context.WeatherInfoService,
context.RecommendationInfoService,
context.CalculatorDataService,
context.SettingsFacade,
context.PlacementId);
return true;
}
@@ -86,7 +103,7 @@ internal sealed class ComponentLibraryService : IComponentLibraryService
}
}
internal sealed class ComponentLibraryWindowService : IComponentLibraryWindowService
internal sealed class EmbeddedComponentLibraryService : IEmbeddedComponentLibraryService
{
public void Open(MainWindow window)
{
@@ -112,3 +129,30 @@ internal sealed class ComponentLibraryWindowService : IComponentLibraryWindowSer
window.OpenComponentLibraryWindowFromService();
}
}
internal sealed class DetachedComponentLibraryWindowService : IDetachedComponentLibraryWindowService
{
public void Open(MainWindow window)
{
ArgumentNullException.ThrowIfNull(window);
window.OpenDetachedComponentLibraryWindowFromService();
}
public void Close(MainWindow window)
{
ArgumentNullException.ThrowIfNull(window);
window.CloseDetachedComponentLibraryWindowFromService();
}
public void Toggle(MainWindow window)
{
ArgumentNullException.ThrowIfNull(window);
if (window.IsDetachedComponentLibraryWindowOpenFromService)
{
window.CloseDetachedComponentLibraryWindowFromService();
return;
}
window.OpenDetachedComponentLibraryWindowFromService();
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Reflection;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Services;
@@ -19,6 +20,11 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
_settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
}
public ComponentSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
internal ComponentSettingsService(string settingsDirectory)
{
if (string.IsNullOrWhiteSpace(settingsDirectory))

View File

@@ -33,7 +33,8 @@ public static class DesktopComponentRegistryFactory
public static DesktopComponentRuntimeRegistry CreateRuntimeRegistry(
ComponentRegistry componentRegistry,
PluginRuntimeService? pluginRuntimeService)
PluginRuntimeService? pluginRuntimeService,
ISettingsFacadeService settingsFacade)
{
var registrations = DesktopComponentRuntimeRegistry.GetDefaultRegistrations().ToList();
var registeredIds = new HashSet<string>(
@@ -65,6 +66,7 @@ public static class DesktopComponentRegistryFactory
}
}
_ = settingsFacade;
return new DesktopComponentRuntimeRegistry(componentRegistry, registrations);
}
@@ -116,7 +118,7 @@ public static class DesktopComponentRegistryFactory
try
{
var settingsService = contribution.Plugin.Services.GetService(typeof(ISettingsService)) as ISettingsService
?? HostSettingsFacadeProvider.GetOrCreate().Settings;
?? context.SettingsService;
var pluginSettings = new PluginScopedSettingsService(
contribution.Plugin.Manifest.Id,
settingsService);

View File

@@ -0,0 +1,14 @@
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Services;
internal static class HostComponentSettingsStoreProvider
{
private static readonly IComponentInstanceSettingsStore Instance =
new ComponentSettingsService(HostSettingsFacadeProvider.GetOrCreate().Settings);
public static IComponentInstanceSettingsStore GetOrCreate()
{
return Instance;
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Services;
public sealed record ComponentLibraryComponentEntry(
string ComponentId,
string DisplayName,
string? DisplayNameLocalizationKey,
string CategoryId,
int MinWidthCells,
int MinHeightCells);
public sealed record ComponentLibraryCategoryEntry(
string Id,
IReadOnlyList<ComponentLibraryComponentEntry> Components);
public sealed record ComponentLibraryCreateContext(
double CellSize,
TimeZoneService TimeZoneService,
IWeatherInfoService WeatherInfoService,
IRecommendationInfoService RecommendationInfoService,
ICalculatorDataService CalculatorDataService,
ISettingsFacadeService SettingsFacade,
string? PlacementId = null);
public interface IComponentLibraryService
{
IReadOnlyList<DesktopComponentDefinition> GetDefinitions();
IReadOnlyList<ComponentLibraryCategoryEntry> GetDesktopCategories();
bool TryCreateControl(
string componentId,
ComponentLibraryCreateContext context,
out Control? control,
out Exception? exception);
}

View File

@@ -19,10 +19,7 @@ internal sealed class SettingsCatalogService : ISettingsCatalog
new SettingsSectionDefinition("appearance", SettingsCategories.Appearance, SettingsScope.App, "settings.appearance.title", iconKey: "DesignIdeas", sortOrder: 10),
new SettingsSectionDefinition("components", SettingsCategories.Components, SettingsScope.ComponentInstance, "settings.components.title", iconKey: "GridDots", sortOrder: 20),
new SettingsSectionDefinition("plugins", SettingsCategories.Plugins, SettingsScope.Plugin, "settings.plugins.title", iconKey: "PuzzlePiece", sortOrder: 30),
new SettingsSectionDefinition("plugin-market", SettingsCategories.PluginMarket, SettingsScope.Plugin, "settings.plugin_market.title", iconKey: "Shop", sortOrder: 40),
new SettingsSectionDefinition("update", SettingsCategories.Update, SettingsScope.App, "settings.update.title", iconKey: "ArrowSync", sortOrder: 50),
new SettingsSectionDefinition("about", SettingsCategories.About, SettingsScope.App, "settings.about.title", iconKey: "Info", sortOrder: 60),
new SettingsSectionDefinition("advanced", SettingsCategories.Advanced, SettingsScope.App, "settings.advanced.title", iconKey: "DeveloperBoard", sortOrder: 70)
new SettingsSectionDefinition("about", SettingsCategories.About, SettingsScope.App, "settings.about.title", iconKey: "Info", sortOrder: 40)
]);
}

View File

@@ -16,7 +16,7 @@ public enum WallpaperMediaType
public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent);
public sealed record WallpaperSettingsState(string? WallpaperPath, string Placement);
public sealed record ThemeAppearanceSettingsState(bool IsNightMode, string? ThemeColor);
public sealed record ThemeAppearanceSettingsState(bool IsNightMode, string? ThemeColor, bool UseSystemChrome);
public sealed record StatusBarSettingsState(
IReadOnlyList<string> TopStatusComponentIds,
IReadOnlyList<string> PinnedTaskbarActions,

View File

@@ -7,18 +7,24 @@ using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.PluginMarket;
namespace LanMountainDesktop.Services.Settings;
internal sealed class GridSettingsService : IGridSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
private readonly DesktopGridLayoutService _gridLayoutService = new();
public GridSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public GridSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new GridSettingsState(
snapshot.GridShortSideCells,
snapshot.GridSpacingPreset,
@@ -27,11 +33,19 @@ internal sealed class GridSettingsService : IGridSettingsService
public void Save(GridSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.GridShortSideCells = state.ShortSideCells;
snapshot.GridSpacingPreset = state.SpacingPreset;
snapshot.DesktopEdgeInsetPercent = state.EdgeInsetPercent;
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.GridShortSideCells),
nameof(AppSettingsSnapshot.GridSpacingPreset),
nameof(AppSettingsSnapshot.DesktopEdgeInsetPercent)
]);
}
public string NormalizeSpacingPreset(string? value)
@@ -67,22 +81,34 @@ internal sealed class GridSettingsService : IGridSettingsService
internal sealed class WallpaperSettingsService : IWallpaperSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
public WallpaperSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public WallpaperSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new WallpaperSettingsState(snapshot.WallpaperPath, snapshot.WallpaperPlacement);
}
public void Save(WallpaperSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.WallpaperPath = state.WallpaperPath;
snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement)
? "Fill"
: state.Placement.Trim();
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.WallpaperPath),
nameof(AppSettingsSnapshot.WallpaperPlacement)
]);
}
}
@@ -182,24 +208,39 @@ internal sealed class WallpaperMediaService : IWallpaperMediaService
internal sealed class ThemeAppearanceService : IThemeAppearanceService
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
private readonly MonetColorService _monetColorService = new();
private readonly WallpaperMediaService _wallpaperMediaService = new();
public ThemeAppearanceService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public ThemeAppearanceSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new ThemeAppearanceSettingsState(
snapshot.IsNightMode ?? false,
snapshot.ThemeColor);
snapshot.ThemeColor,
snapshot.UseSystemChrome);
}
public void Save(ThemeAppearanceSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.IsNightMode = state.IsNightMode;
snapshot.ThemeColor = state.ThemeColor;
_appSettingsService.Save(snapshot);
snapshot.UseSystemChrome = state.UseSystemChrome;
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.IsNightMode),
nameof(AppSettingsSnapshot.ThemeColor),
nameof(AppSettingsSnapshot.UseSystemChrome)
]);
}
public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath)
@@ -236,11 +277,16 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
internal sealed class StatusBarSettingsService : IStatusBarSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
public StatusBarSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public StatusBarSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new StatusBarSettingsState(
snapshot.TopStatusComponentIds?.ToArray() ?? [],
snapshot.PinnedTaskbarActions?.ToArray() ?? [],
@@ -253,7 +299,7 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
public void Save(StatusBarSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.TopStatusComponentIds = state.TopStatusComponentIds?.ToList() ?? [];
snapshot.PinnedTaskbarActions = state.PinnedTaskbarActions?.ToList() ?? [];
snapshot.EnableDynamicTaskbarActions = state.EnableDynamicTaskbarActions;
@@ -261,7 +307,19 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
snapshot.ClockDisplayFormat = state.ClockDisplayFormat;
snapshot.StatusBarSpacingMode = state.SpacingMode;
snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent;
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.TopStatusComponentIds),
nameof(AppSettingsSnapshot.PinnedTaskbarActions),
nameof(AppSettingsSnapshot.EnableDynamicTaskbarActions),
nameof(AppSettingsSnapshot.TaskbarLayoutMode),
nameof(AppSettingsSnapshot.ClockDisplayFormat),
nameof(AppSettingsSnapshot.StatusBarSpacingMode),
nameof(AppSettingsSnapshot.StatusBarCustomSpacingPercent)
]);
}
}
@@ -295,12 +353,17 @@ internal sealed class WeatherProviderAdapter : IWeatherProvider, IWeatherInfoSer
internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposable
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
private readonly WeatherProviderAdapter _weatherProvider = new();
public WeatherSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public WeatherSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new WeatherSettingsState(
snapshot.WeatherLocationMode,
snapshot.WeatherLocationKey,
@@ -316,7 +379,7 @@ internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposa
public void Save(WeatherSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.WeatherLocationMode = state.LocationMode;
snapshot.WeatherLocationKey = state.LocationKey;
snapshot.WeatherLocationName = state.LocationName;
@@ -327,7 +390,22 @@ internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposa
snapshot.WeatherIconPackId = state.IconPackId;
snapshot.WeatherNoTlsRequests = state.NoTlsRequests;
snapshot.WeatherLocationQuery = state.LocationQuery;
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.WeatherLocationMode),
nameof(AppSettingsSnapshot.WeatherLocationKey),
nameof(AppSettingsSnapshot.WeatherLocationName),
nameof(AppSettingsSnapshot.WeatherLatitude),
nameof(AppSettingsSnapshot.WeatherLongitude),
nameof(AppSettingsSnapshot.WeatherAutoRefreshLocation),
nameof(AppSettingsSnapshot.WeatherExcludedAlerts),
nameof(AppSettingsSnapshot.WeatherIconPackId),
nameof(AppSettingsSnapshot.WeatherNoTlsRequests),
nameof(AppSettingsSnapshot.WeatherLocationQuery)
]);
}
public IWeatherInfoService GetWeatherInfoService()
@@ -343,41 +421,74 @@ internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposa
internal sealed class RegionSettingsService : IRegionSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
private readonly TimeZoneService _timeZoneService = new();
public RegionSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
ApplyTimeZone(_settingsService.Load().TimeZoneId);
}
public RegionSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new RegionSettingsState(snapshot.LanguageCode, snapshot.TimeZoneId);
}
public void Save(RegionSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.LanguageCode = string.IsNullOrWhiteSpace(state.LanguageCode)
? "zh-CN"
: state.LanguageCode.Trim();
snapshot.TimeZoneId = string.IsNullOrWhiteSpace(state.TimeZoneId)
? null
: state.TimeZoneId.Trim();
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.LanguageCode),
nameof(AppSettingsSnapshot.TimeZoneId)
]);
ApplyTimeZone(snapshot.TimeZoneId);
}
public TimeZoneService GetTimeZoneService()
{
return _timeZoneService;
}
private void ApplyTimeZone(string? timeZoneId)
{
if (string.IsNullOrWhiteSpace(timeZoneId))
{
_timeZoneService.CurrentTimeZone = TimeZoneInfo.Local;
return;
}
if (!_timeZoneService.SetTimeZoneById(timeZoneId))
{
_timeZoneService.CurrentTimeZone = TimeZoneInfo.Local;
}
}
}
internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
private readonly GitHubReleaseUpdateService _releaseUpdateService = new("wwiinnddyy", "LanMountainDesktop");
public UpdateSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public UpdateSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new UpdateSettingsState(
snapshot.AutoCheckUpdates,
snapshot.IncludePrereleaseUpdates,
@@ -386,11 +497,19 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
public void Save(UpdateSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.AutoCheckUpdates = state.AutoCheckUpdates;
snapshot.IncludePrereleaseUpdates = state.IncludePrereleaseUpdates;
snapshot.UpdateChannel = state.UpdateChannel;
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.AutoCheckUpdates),
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
nameof(AppSettingsSnapshot.UpdateChannel)
]);
}
public Task<UpdateCheckResult> CheckForUpdatesAsync(
@@ -443,11 +562,12 @@ internal sealed class LauncherPolicyService : ILauncherPolicyService
internal sealed class PluginManagementSettingsService : IPluginManagementSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
private PluginRuntimeService? _pluginRuntimeService;
public PluginManagementSettingsService(PluginRuntimeService? pluginRuntimeService)
public PluginManagementSettingsService(ISettingsService settingsService, PluginRuntimeService? pluginRuntimeService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
_pluginRuntimeService = pluginRuntimeService;
}
@@ -458,15 +578,18 @@ internal sealed class PluginManagementSettingsService : IPluginManagementSetting
public PluginManagementSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new PluginManagementSettingsState(snapshot.DisabledPluginIds?.ToArray() ?? []);
}
public void Save(PluginManagementSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.DisabledPluginIds = state.DisabledPluginIds?.ToList() ?? [];
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys: [nameof(AppSettingsSnapshot.DisabledPluginIds)]);
}
public IReadOnlyList<InstalledPluginInfo> GetInstalledPlugins()
@@ -653,19 +776,19 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
{
Settings = new SettingsService();
Catalog = new SettingsCatalogService();
Grid = new GridSettingsService();
Wallpaper = new WallpaperSettingsService();
Grid = new GridSettingsService(Settings);
Wallpaper = new WallpaperSettingsService(Settings);
WallpaperMedia = new WallpaperMediaService();
Theme = new ThemeAppearanceService();
StatusBar = new StatusBarSettingsService();
_weatherSettingsService = new WeatherSettingsService();
Theme = new ThemeAppearanceService(Settings);
StatusBar = new StatusBarSettingsService(Settings);
_weatherSettingsService = new WeatherSettingsService(Settings);
Weather = _weatherSettingsService;
Region = new RegionSettingsService();
_updateSettingsService = new UpdateSettingsService();
Region = new RegionSettingsService(Settings);
_updateSettingsService = new UpdateSettingsService(Settings);
Update = _updateSettingsService;
LauncherCatalog = new LauncherCatalogService();
LauncherPolicy = new LauncherPolicyService();
_pluginManagementSettingsService = new PluginManagementSettingsService(pluginRuntimeService);
_pluginManagementSettingsService = new PluginManagementSettingsService(Settings, pluginRuntimeService);
PluginManagement = _pluginManagementSettingsService;
_pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService);
PluginMarket = _pluginMarketSettingsService;

View File

@@ -0,0 +1,334 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Plugins;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views.SettingsPages;
using Microsoft.Extensions.DependencyInjection;
namespace LanMountainDesktop.Services.Settings;
public sealed class SettingsPageDescriptor
{
private readonly Func<ISettingsPageHostContext, Control> _factory;
public SettingsPageDescriptor(
string pageId,
string title,
string? description,
string iconKey,
string? selectedIconKey,
SettingsPageCategory category,
int sortOrder,
string? pluginId,
bool isBuiltIn,
bool hideDefault,
bool hidePageTitle,
bool useFullWidth,
string? groupId,
Func<ISettingsPageHostContext, Control> factory)
{
ArgumentException.ThrowIfNullOrWhiteSpace(pageId);
ArgumentException.ThrowIfNullOrWhiteSpace(title);
ArgumentException.ThrowIfNullOrWhiteSpace(iconKey);
ArgumentNullException.ThrowIfNull(factory);
PageId = pageId.Trim();
Title = title.Trim();
Description = string.IsNullOrWhiteSpace(description) ? null : description.Trim();
IconKey = iconKey.Trim();
SelectedIconKey = string.IsNullOrWhiteSpace(selectedIconKey) ? IconKey : selectedIconKey.Trim();
Category = category;
SortOrder = sortOrder;
PluginId = string.IsNullOrWhiteSpace(pluginId) ? null : pluginId.Trim();
IsBuiltIn = isBuiltIn;
HideDefault = hideDefault;
HidePageTitle = hidePageTitle;
UseFullWidth = useFullWidth;
GroupId = string.IsNullOrWhiteSpace(groupId) ? null : groupId.Trim();
_factory = factory;
}
public string PageId { get; }
public string Title { get; }
public string? Description { get; }
public string IconKey { get; }
public string SelectedIconKey { get; }
public SettingsPageCategory Category { get; }
public int SortOrder { get; }
public string? PluginId { get; }
public bool IsBuiltIn { get; }
public bool HideDefault { get; }
public bool HidePageTitle { get; }
public bool UseFullWidth { get; }
public string? GroupId { get; }
public Control CreatePage(ISettingsPageHostContext hostContext) => _factory(hostContext);
}
public interface ISettingsPageRegistry
{
void Rebuild();
IReadOnlyList<SettingsPageDescriptor> GetPages();
bool TryGetPage(string pageId, out SettingsPageDescriptor? descriptor);
}
internal sealed class SettingsPageRegistry : ISettingsPageRegistry, IDisposable
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
private readonly LocalizationService _localizationService;
private readonly Func<PluginRuntimeService?> _pluginRuntimeAccessor;
private readonly object _gate = new();
private readonly List<SettingsPageDescriptor> _pages = [];
private ServiceProvider? _hostServices;
public SettingsPageRegistry(
ISettingsFacadeService settingsFacade,
IHostApplicationLifecycle hostApplicationLifecycle,
LocalizationService localizationService,
Func<PluginRuntimeService?> pluginRuntimeAccessor)
{
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
_hostApplicationLifecycle = hostApplicationLifecycle ?? throw new ArgumentNullException(nameof(hostApplicationLifecycle));
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
_pluginRuntimeAccessor = pluginRuntimeAccessor ?? throw new ArgumentNullException(nameof(pluginRuntimeAccessor));
}
public void Rebuild()
{
lock (_gate)
{
_pages.Clear();
RebuildHostServices();
RegisterAssemblyPages(
typeof(App).Assembly,
_hostServices!,
pluginId: null,
isBuiltIn: true);
var pluginRuntime = _pluginRuntimeAccessor();
if (pluginRuntime is null)
{
SortPages();
return;
}
foreach (var loadedPlugin in pluginRuntime.LoadedPlugins)
{
RegisterPluginPages(loadedPlugin);
RegisterLegacyPluginSections(loadedPlugin);
}
SortPages();
}
}
public IReadOnlyList<SettingsPageDescriptor> GetPages()
{
lock (_gate)
{
return _pages.ToArray();
}
}
public bool TryGetPage(string pageId, out SettingsPageDescriptor? descriptor)
{
ArgumentException.ThrowIfNullOrWhiteSpace(pageId);
lock (_gate)
{
descriptor = _pages.FirstOrDefault(item =>
string.Equals(item.PageId, pageId, StringComparison.OrdinalIgnoreCase));
return descriptor is not null;
}
}
public void Dispose()
{
_hostServices?.Dispose();
}
private void RebuildHostServices()
{
_hostServices?.Dispose();
var services = new ServiceCollection();
services.AddSingleton(_settingsFacade);
services.AddSingleton(_settingsFacade.Settings);
services.AddSingleton(_settingsFacade.Catalog);
services.AddSingleton(_hostApplicationLifecycle);
services.AddSingleton(_localizationService);
var pluginRuntime = _pluginRuntimeAccessor();
if (pluginRuntime is not null)
{
services.AddSingleton(pluginRuntime);
}
_hostServices = services.BuildServiceProvider(new ServiceProviderOptions
{
ValidateScopes = false,
ValidateOnBuild = false
});
}
private void RegisterAssemblyPages(
Assembly assembly,
IServiceProvider services,
string? pluginId,
bool isBuiltIn)
{
foreach (var pageType in assembly.GetTypes()
.Where(type => !type.IsAbstract && typeof(SettingsPageBase).IsAssignableFrom(type)))
{
var pageInfo = pageType.GetCustomAttribute<SettingsPageInfoAttribute>();
if (pageInfo is null)
{
continue;
}
var category = isBuiltIn ? pageInfo.Category : SettingsPageCategory.Plugins;
var sortOrder = isBuiltIn ? pageInfo.SortOrder : 100 + pageInfo.SortOrder;
var title = ResolveLocalizedText(pageInfo.TitleLocalizationKey, pageInfo.Name);
var description = ResolveLocalizedText(pageInfo.DescriptionLocalizationKey, null);
_pages.Add(new SettingsPageDescriptor(
pageInfo.Id,
title,
description,
pageInfo.IconKey,
pageInfo.SelectedIconKey,
category,
sortOrder,
pluginId,
isBuiltIn,
pageInfo.HideDefault,
pageInfo.HidePageTitle,
pageInfo.UseFullWidth,
pageInfo.GroupId,
hostContext => CreatePage(services, pageType, hostContext)));
}
}
private void RegisterPluginPages(LoadedPlugin loadedPlugin)
{
RegisterAssemblyPages(
loadedPlugin.Assembly,
loadedPlugin.Services,
loadedPlugin.Manifest.Id,
isBuiltIn: false);
}
private void RegisterLegacyPluginSections(LoadedPlugin loadedPlugin)
{
var localizer = PluginLocalizer.Create(loadedPlugin.RuntimeContext);
foreach (var section in loadedPlugin.SettingsSections)
{
var pageId = $"plugin:{loadedPlugin.Manifest.Id}:{section.Id}";
var title = localizer.GetString(section.TitleLocalizationKey, section.TitleLocalizationKey);
var description = string.IsNullOrWhiteSpace(section.DescriptionLocalizationKey)
? null
: localizer.GetString(section.DescriptionLocalizationKey, section.DescriptionLocalizationKey);
_pages.Add(new SettingsPageDescriptor(
pageId,
title,
description,
section.IconKey,
section.IconKey,
SettingsPageCategory.Plugins,
200 + section.SortOrder,
loadedPlugin.Manifest.Id,
isBuiltIn: false,
hideDefault: false,
hidePageTitle: false,
useFullWidth: false,
groupId: null,
hostContext =>
{
var page = new GeneratedPluginSettingsPage(
new PluginGeneratedSettingsPageViewModel(
_settingsFacade.Settings,
loadedPlugin.Manifest.Id,
section,
localizer));
page.InitializeHostContext(hostContext);
return page;
}));
}
}
private void SortPages()
{
_pages.Sort(static (left, right) =>
{
var categoryCompare = left.Category.CompareTo(right.Category);
if (categoryCompare != 0)
{
return categoryCompare;
}
var sortOrderCompare = left.SortOrder.CompareTo(right.SortOrder);
if (sortOrderCompare != 0)
{
return sortOrderCompare;
}
var pluginCompare = string.Compare(left.PluginId, right.PluginId, StringComparison.OrdinalIgnoreCase);
if (pluginCompare != 0)
{
return pluginCompare;
}
return string.Compare(left.PageId, right.PageId, StringComparison.OrdinalIgnoreCase);
});
}
private string ResolveLocalizedText(string? localizationKey, string? fallback)
{
if (string.IsNullOrWhiteSpace(localizationKey))
{
return fallback ?? string.Empty;
}
var languageCode = _settingsFacade.Region.Get().LanguageCode;
var normalizedLanguageCode = _localizationService.NormalizeLanguageCode(languageCode);
return _localizationService.GetString(
normalizedLanguageCode,
localizationKey,
string.IsNullOrWhiteSpace(fallback) ? localizationKey : fallback);
}
private static Control CreatePage(
IServiceProvider services,
Type pageType,
ISettingsPageHostContext hostContext)
{
var page = (Control)ActivatorUtilities.CreateInstance(services, pageType);
if (page is SettingsPageBase settingsPage)
{
settingsPage.InitializeHostContext(hostContext);
}
return page;
}
}

View File

@@ -0,0 +1,20 @@
using System;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services;
public static class SettingsServiceAppSnapshotExtensions
{
public static AppSettingsSnapshot Load(this ISettingsService settingsService)
{
ArgumentNullException.ThrowIfNull(settingsService);
return settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
}
public static void Save(this ISettingsService settingsService, AppSettingsSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(settingsService);
settingsService.SaveSnapshot(SettingsScope.App, snapshot ?? new AppSettingsSnapshot());
}
}

View File

@@ -0,0 +1,306 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views;
namespace LanMountainDesktop.Services.Settings;
public enum SettingsWindowAnchorTarget
{
DesktopDockTrailingEdge = 0
}
public enum SettingsWindowFallbackMode
{
None = 0,
ScreenBottomRight = 1
}
public readonly record struct SettingsWindowOpenRequest(
string Source,
Window? Owner = null,
string? PageId = null,
SettingsWindowAnchorTarget AnchorTarget = SettingsWindowAnchorTarget.DesktopDockTrailingEdge,
SettingsWindowFallbackMode FallbackMode = SettingsWindowFallbackMode.ScreenBottomRight);
public interface ISettingsWindowAnchorProvider
{
bool TryGetSettingsWindowAnchorBounds(out PixelRect anchorBounds);
}
public interface ISettingsWindowService
{
bool IsOpen { get; }
event EventHandler? StateChanged;
void Open(SettingsWindowOpenRequest request);
void Close();
void Toggle(SettingsWindowOpenRequest request);
}
internal sealed class SettingsWindowService : ISettingsWindowService
{
private readonly ISettingsPageRegistry _pageRegistry;
private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
private readonly ISettingsFacadeService _settingsFacade;
private readonly LocalizationService _localizationService;
private SettingsWindowViewModel _viewModel = null!;
private SettingsWindow? _window;
public SettingsWindowService(
ISettingsPageRegistry pageRegistry,
IHostApplicationLifecycle hostApplicationLifecycle,
ISettingsFacadeService settingsFacade)
{
_pageRegistry = pageRegistry;
_hostApplicationLifecycle = hostApplicationLifecycle;
_settingsFacade = settingsFacade;
_localizationService = new();
_settingsFacade.Settings.Changed += OnSettingsChanged;
}
private string L(string key)
{
var regionState = _settingsFacade.Region.Get();
var languageCode = regionState.LanguageCode ?? "zh-CN";
return _localizationService.GetString(languageCode, key, key);
}
public bool IsOpen => _window is { IsVisible: true };
public event EventHandler? StateChanged;
public void Open(SettingsWindowOpenRequest request)
{
_pageRegistry.Rebuild();
_window ??= CreateWindow();
var themeState = _settingsFacade.Theme.Get();
_window.ApplyChromeMode(themeState.UseSystemChrome);
ApplyTheme(_window, themeState.IsNightMode);
_window.ReloadPages(request.PageId);
PositionWindow(_window, request);
if (!_window.IsVisible)
{
if (request.Owner is not null && request.Owner.IsVisible)
{
_window.Show(request.Owner);
}
else
{
_window.Show();
}
NotifyStateChanged();
PositionWindowLater(_window, request);
return;
}
_window.Activate();
PositionWindowLater(_window, request);
}
public void Close()
{
_window?.Close();
}
public void Toggle(SettingsWindowOpenRequest request)
{
if (IsOpen)
{
Close();
return;
}
Open(request);
}
private SettingsWindow CreateWindow()
{
var regionState = _settingsFacade.Region.Get();
var languageCode = regionState.LanguageCode ?? "zh-CN";
_viewModel = new SettingsWindowViewModel(_localizationService, languageCode).Initialize();
var themeState = _settingsFacade.Theme.Get();
var useSystemChrome = themeState.UseSystemChrome;
var window = new SettingsWindow(
_viewModel,
_pageRegistry,
_hostApplicationLifecycle,
useSystemChrome);
ApplyTheme(window, themeState.IsNightMode);
window.ShowInTaskbar = false;
window.Closed += (_, _) =>
{
_window = null;
NotifyStateChanged();
};
return window;
}
private void PositionWindowLater(SettingsWindow window, SettingsWindowOpenRequest request)
{
Dispatcher.UIThread.Post(
() =>
{
if (!window.IsVisible)
{
return;
}
PositionWindow(window, request);
},
DispatcherPriority.Background);
}
private static void PositionWindow(SettingsWindow window, SettingsWindowOpenRequest request)
{
if (request.AnchorTarget == SettingsWindowAnchorTarget.DesktopDockTrailingEdge &&
request.Owner is ISettingsWindowAnchorProvider anchorProvider &&
anchorProvider.TryGetSettingsWindowAnchorBounds(out var anchorBounds))
{
PositionWindowAboveAnchor(window, anchorBounds, request);
return;
}
if (request.FallbackMode == SettingsWindowFallbackMode.ScreenBottomRight)
{
PositionWindowNearScreenBottomRight(window, request);
}
}
private static void PositionWindowAboveAnchor(Window window, PixelRect anchorBounds, SettingsWindowOpenRequest request)
{
var workingArea = GetWorkingArea(window, request);
if (anchorBounds.Width <= 0 || anchorBounds.Height <= 0 ||
anchorBounds.Right < workingArea.X || anchorBounds.Y > workingArea.Bottom)
{
PositionWindowNearScreenBottomRight(window, request);
return;
}
var scale = window.RenderScaling > 0 ? window.RenderScaling : 1d;
var width = ResolveWindowWidth(window, scale);
var height = ResolveWindowHeight(window, scale);
var inset = (int)Math.Round(24 * scale);
var gap = (int)Math.Round(16 * scale);
var x = anchorBounds.Right - width - inset;
var y = anchorBounds.Y - height - gap;
x = Math.Clamp(x, workingArea.X + inset, Math.Max(workingArea.X + inset, workingArea.Right - width - inset));
y = Math.Clamp(y, workingArea.Y + inset, Math.Max(workingArea.Y + inset, workingArea.Bottom - height - inset));
window.Position = new PixelPoint(x, y);
}
private static void PositionWindowNearScreenBottomRight(Window window, SettingsWindowOpenRequest request)
{
var workingArea = GetWorkingArea(window, request);
var scale = window.RenderScaling > 0 ? window.RenderScaling : 1d;
var width = ResolveWindowWidth(window, scale);
var height = ResolveWindowHeight(window, scale);
var inset = (int)Math.Round(24 * scale);
var x = Math.Max(workingArea.X + inset, workingArea.Right - width - inset);
var y = Math.Max(workingArea.Y + inset, workingArea.Bottom - height - inset);
window.Position = new PixelPoint(x, y);
}
private static PixelRect GetWorkingArea(Window window, SettingsWindowOpenRequest request)
{
if (request.Owner is not null && request.Owner.Screens?.ScreenFromWindow(request.Owner) is { } ownerScreen)
{
return ownerScreen.WorkingArea;
}
if (window.Screens?.ScreenFromWindow(window) is { } windowScreen)
{
return windowScreen.WorkingArea;
}
return window.Screens?.Primary?.WorkingArea
?? new PixelRect(
0,
0,
Math.Max(1280, ResolveWindowWidth(window, 1d) + 96),
Math.Max(720, ResolveWindowHeight(window, 1d) + 96));
}
private static int ResolveWindowWidth(Window window, double scale)
{
var widthDip = window.Bounds.Width > 1 ? window.Bounds.Width : Math.Max(window.Width, window.MinWidth);
return Math.Max(320, (int)Math.Round(widthDip * scale));
}
private static int ResolveWindowHeight(Window window, double scale)
{
var heightDip = window.Bounds.Height > 1 ? window.Bounds.Height : Math.Max(window.Height, window.MinHeight);
return Math.Max(240, (int)Math.Round(heightDip * scale));
}
private void NotifyStateChanged()
{
StateChanged?.Invoke(this, EventArgs.Empty);
}
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
{
_ = sender;
if (e.Scope != SettingsScope.App)
{
return;
}
Dispatcher.UIThread.Post(() =>
{
if (_window is null || _viewModel is null)
{
return;
}
var changedKeys = e.ChangedKeys?.ToArray();
var refreshAll = changedKeys is null || changedKeys.Length == 0;
var languageChanged = refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
var themeChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase);
if (languageChanged)
{
var regionState = _settingsFacade.Region.Get();
_viewModel.RefreshLanguage(regionState.LanguageCode);
_pageRegistry.Rebuild();
_window.ReloadPages(_viewModel.CurrentPageId);
_window.RefreshShellText();
}
if (themeChanged)
{
var themeState = _settingsFacade.Theme.Get();
_window.ApplyChromeMode(themeState.UseSystemChrome);
ApplyTheme(_window, themeState.IsNightMode);
}
}, DispatcherPriority.Background);
}
private static void ApplyTheme(SettingsWindow window, bool isNightMode)
{
window.RequestedThemeVariant = isNightMode
? ThemeVariant.Dark
: ThemeVariant.Light;
}
}

View File

@@ -53,6 +53,20 @@
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonHoverBackgroundBrush}" />
</Style>
<Style Selector="TextBox">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
<Style Selector="NumericUpDown">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
<Style Selector="ui|NumberBox">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
<Setter Property="BorderThickness" Value="1" />
@@ -60,11 +74,15 @@
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
<Style Selector="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
<Style Selector="ToggleSwitch">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
<Style Selector="Grid.settings-scope Border.settings-expander-shell">
<Style Selector=".settings-scope Border.settings-expander-shell">
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
@@ -73,28 +91,38 @@
<Setter Property="Margin" Value="0,0,0,10" />
</Style>
<Style Selector="Grid.settings-scope Button">
<Style Selector=".settings-scope Button">
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
<Setter Property="MinHeight" Value="34" />
</Style>
<Style Selector="Grid.settings-scope ComboBox">
<Style Selector=".settings-scope ComboBox">
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
<Setter Property="MinHeight" Value="34" />
</Style>
<Style Selector="Grid.settings-scope ComboBoxItem">
<Style Selector=".settings-scope TextBox">
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
<Setter Property="MinHeight" Value="34" />
</Style>
<Style Selector=".settings-scope ComboBoxItem">
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" />
<Setter Property="Padding" Value="10,6" />
<Setter Property="Margin" Value="4,2" />
</Style>
<Style Selector="Grid.settings-scope ui|NumberBox">
<Style Selector=".settings-scope ui|NumberBox">
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
<Setter Property="MinHeight" Value="34" />
</Style>
<Style Selector="Grid.settings-scope RadioButton">
<Style Selector=".settings-scope NumericUpDown">
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
<Setter Property="MinHeight" Value="34" />
</Style>
<Style Selector=".settings-scope RadioButton">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
@@ -103,11 +131,11 @@
<Setter Property="MinHeight" Value="34" />
</Style>
<Style Selector="Grid.settings-scope RadioButton:pointerover">
<Style Selector=".settings-scope RadioButton:pointerover">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonHoverBackgroundBrush}" />
</Style>
<Style Selector="Grid.settings-scope RadioButton:checked">
<Style Selector=".settings-scope RadioButton:checked">
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
</Style>

View File

@@ -71,7 +71,7 @@
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
</Style>
<Style Selector="Grid.settings-scope ComboBox">
<Style Selector=".settings-scope ComboBox">
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" Easing="0.22,1,0.36,1" />
@@ -80,11 +80,11 @@
</Setter>
</Style>
<Style Selector="Grid.settings-scope ComboBox:pointerover">
<Style Selector=".settings-scope ComboBox:pointerover">
<Setter Property="RenderTransform" Value="scale(1.01)" />
</Style>
<Style Selector="Grid.settings-scope ToggleSwitch">
<Style Selector=".settings-scope ToggleSwitch">
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.22,1,0.36,1" />
@@ -93,7 +93,7 @@
</Setter>
</Style>
<Style Selector="Grid.settings-scope ToggleSwitch:pointerover">
<Style Selector=".settings-scope ToggleSwitch:pointerover">
<Setter Property="RenderTransform" Value="scale(1.01)" />
</Style>
</Styles>

View File

@@ -0,0 +1,198 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:FluentAvalonia.UI.Controls">
<Style Selector="StackPanel.settings-page-container">
<Setter Property="Spacing" Value="0" />
<Setter Property="Margin" Value="12,14,28,32" />
<Setter Property="MaxWidth" Value="900" />
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
<Style Selector="TextBlock.settings-section-title">
<Setter Property="FontSize" Value="30" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="Margin" Value="0,0,0,8" />
</Style>
<Style Selector="TextBlock.settings-section-description">
<Setter Property="FontSize" Value="14" />
<Setter Property="Opacity" Value="0.76" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="MaxWidth" Value="760" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="Margin" Value="0,0,0,22" />
</Style>
<Style Selector="TextBlock.settings-subsection-title">
<Setter Property="FontSize" Value="15" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="Margin" Value="0,18,0,10" />
</Style>
<Style Selector="Border.settings-section-card, Border.settings-option-card, Border.settings-list-item">
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background"
Duration="{StaticResource FluttermotionToken.Duration.Standard}"
Easing="0.22,1,0.36,1" />
<BoxShadowsTransition Property="BoxShadow"
Duration="{StaticResource FluttermotionToken.Duration.Fast}"
Easing="0.22,1,0.36,1" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.settings-section-card">
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
<Setter Property="CornerRadius" Value="18" />
<Setter Property="Padding" Value="20" />
<Setter Property="Margin" Value="0,0,0,14" />
<Setter Property="BoxShadow" Value="0 2 8 #12000000" />
</Style>
<Style Selector="Border.settings-option-card">
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Padding" Value="16" />
<Setter Property="Margin" Value="0,0,0,12" />
<Setter Property="BoxShadow" Value="0 1 4 #0F000000" />
</Style>
<Style Selector="Border.settings-list-item">
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Padding" Value="16" />
<Setter Property="Margin" Value="0,0,0,10" />
<Setter Property="BoxShadow" Value="0 1 4 #0F000000" />
</Style>
<Style Selector="Border.settings-list-item:pointerover, Border.settings-option-card:pointerover">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
<Setter Property="BoxShadow" Value="0 4 10 #13000000" />
</Style>
<Style Selector="Border.settings-option-card-icon-host, Border.settings-section-card-icon-host">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Top" />
</Style>
<Style Selector="Border.settings-option-card-icon-host">
<Setter Property="Width" Value="36" />
<Setter Property="Height" Value="36" />
</Style>
<Style Selector="Border.settings-section-card-icon-host">
<Setter Property="Width" Value="42" />
<Setter Property="Height" Value="42" />
</Style>
<Style Selector="TextBlock.settings-card-header">
<Setter Property="FontSize" Value="17" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="Margin" Value="0,0,0,12" />
</Style>
<Style Selector="TextBlock.settings-card-description">
<Setter Property="FontSize" Value="13" />
<Setter Property="Opacity" Value="0.72" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="Margin" Value="0,-8,0,0" />
</Style>
<Style Selector="TextBlock.settings-item-label">
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
<Style Selector="TextBlock.settings-item-description">
<Setter Property="FontSize" Value="12" />
<Setter Property="Opacity" Value="0.68" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="MaxWidth" Value="620" />
</Style>
<Style Selector="StackPanel.settings-item">
<Setter Property="Spacing" Value="8" />
<Setter Property="Margin" Value="0,0,0,16" />
</Style>
<Style Selector="Grid.settings-inline-pair">
<Setter Property="ColumnSpacing" Value="14" />
</Style>
<Style Selector="Grid.settings-list-actions">
<Setter Property="ColumnSpacing" Value="12" />
</Style>
<Style Selector="ui|SettingsExpander.settings-expander-card">
<Setter Property="Margin" Value="0,0,0,14" />
</Style>
<Style Selector="ui|SettingsExpander.settings-expander-card /template/ ContentPresenter#FooterContentPresenter">
<Setter Property="Margin" Value="0,6,0,2" />
</Style>
<Style Selector="ui|SettingsExpander.settings-expander-card /template/ ContentPresenter#ContentPresenter">
<Setter Property="Margin" Value="0,14,0,0" />
</Style>
<Style Selector="ui|SettingsExpander.settings-expander-card ComboBox, .settings-section-card ComboBox, .settings-option-card ComboBox">
<Setter Property="MinWidth" Value="220" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
<Style Selector="ui|SettingsExpander.settings-expander-card TextBox, .settings-section-card TextBox, .settings-option-card TextBox">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="MinHeight" Value="38" />
</Style>
<Style Selector="ui|SettingsExpander.settings-expander-card NumericUpDown, .settings-section-card NumericUpDown, .settings-option-card NumericUpDown">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="MinHeight" Value="38" />
</Style>
<Style Selector="ui|SettingsExpander.settings-expander-card ToggleSwitch, .settings-option-card ToggleSwitch, .settings-list-item ToggleSwitch">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="OnContent" Value="{x:Null}" />
<Setter Property="OffContent" Value="{x:Null}" />
</Style>
<Style Selector=".settings-section-card Button, .settings-option-card Button, .settings-list-item Button, ui|SettingsExpander.settings-expander-card Button">
<Setter Property="MinHeight" Value="36" />
<Setter Property="Padding" Value="14,8" />
</Style>
<Style Selector="Button.settings-accent-button">
<Setter Property="Background" Value="{DynamicResource AdaptiveAccentBrush}" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveOnAccentBrush}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="16,10" />
<Setter Property="MinHeight" Value="36" />
</Style>
<Style Selector="Button.settings-accent-button:pointerover">
<Setter Property="Background" Value="{DynamicResource AdaptiveAccentLightBrush}" />
</Style>
<Style Selector="Separator.settings-separator">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
<Setter Property="Margin" Value="0,18" />
<Setter Property="Height" Value="1" />
<Setter Property="Opacity" Value="0.5" />
</Style>
</Styles>

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Avalonia.Controls;
using FluentIcons.Common;
namespace LanMountainDesktop.ViewModels;
public sealed class ComponentLibraryWindowViewModel : ViewModelBase
{
public string Title { get; set; } = "Widgets";
public ObservableCollection<ComponentLibraryCategoryViewModel> Categories { get; } = [];
public ObservableCollection<ComponentLibraryItemViewModel> Components { get; } = [];
}
public sealed class ComponentLibraryCategoryViewModel
{
public ComponentLibraryCategoryViewModel(
string id,
string title,
Symbol icon,
IReadOnlyList<ComponentLibraryItemViewModel> components)
{
Id = id;
Title = title;
Icon = icon;
Components = components;
}
public string Id { get; }
public string Title { get; }
public Symbol Icon { get; }
public IReadOnlyList<ComponentLibraryItemViewModel> Components { get; }
}
public sealed class ComponentLibraryItemViewModel
{
public ComponentLibraryItemViewModel(
string componentId,
string displayName,
Control? previewControl)
{
ComponentId = componentId;
DisplayName = displayName;
PreviewControl = previewControl;
}
public string ComponentId { get; }
public string DisplayName { get; }
public Control? PreviewControl { get; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,129 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:fi="using:FluentIcons.Avalonia"
x:Class="LanMountainDesktop.Views.ComponentLibraryWindow"
x:DataType="vm:ComponentLibraryWindowViewModel"
Width="980"
Height="620"
MinWidth="760"
MinHeight="500"
CanResize="True"
SystemDecorations="Full"
Title="Component Library"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<Grid RowDefinitions="Auto,*"
RowSpacing="10"
Margin="14">
<Grid ColumnDefinitions="*,Auto"
Margin="6,4,6,0">
<TextBlock VerticalAlignment="Center"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding Title}" />
<Button Grid.Column="1"
Width="34"
Height="34"
Padding="0"
Background="Transparent"
BorderThickness="0"
Click="OnCloseClick">
<fi:FluentIcon Icon="Dismiss"
IconVariant="Regular" />
</Button>
</Grid>
<Grid Grid.Row="1"
ColumnDefinitions="240,*"
ColumnSpacing="12">
<Border Classes="glass-panel"
CornerRadius="24"
Padding="10">
<ListBox x:Name="CategoryListBox"
Background="Transparent"
BorderThickness="0"
SelectionChanged="OnCategorySelectionChanged"
ItemsSource="{Binding Categories}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
<Border Padding="10"
Margin="0,0,0,6"
CornerRadius="14"
Background="{DynamicResource AdaptiveNavItemBackgroundBrush}">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<fi:SymbolIcon Symbol="{Binding Icon}"
IconVariant="Regular"
FontSize="16" />
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding Title}" />
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<Border Grid.Column="1"
Classes="glass-strong"
CornerRadius="24"
Padding="10">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding Components}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ComponentLibraryItemViewModel">
<Border Width="240"
Height="220"
Margin="6"
CornerRadius="18"
Padding="10"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1">
<Grid RowDefinitions="*,Auto,Auto"
RowSpacing="8">
<Border CornerRadius="12"
Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
BorderThickness="1"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
Padding="8">
<ContentControl HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{Binding PreviewControl}" />
</Border>
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="{Binding DisplayName}" />
<Button Grid.Row="2"
HorizontalAlignment="Center"
Padding="12,6"
Tag="{Binding ComponentId}"
Click="OnAddComponentClick">
<TextBlock Text="Add to Desktop" />
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Interactivity;
using FluentIcons.Common;
using LanMountainDesktop.Services;
using LanMountainDesktop.ViewModels;
namespace LanMountainDesktop.Views;
public partial class ComponentLibraryWindow : Window
{
private IComponentLibraryService? _componentLibraryService;
private Func<double, ComponentLibraryCreateContext>? _createContextFactory;
private Func<string, string, string>? _localize;
private readonly ComponentLibraryWindowViewModel _viewModel = new();
public ComponentLibraryWindow()
{
InitializeComponent();
DataContext = _viewModel;
}
public ComponentLibraryWindow(
IComponentLibraryService componentLibraryService,
Func<double, ComponentLibraryCreateContext> createContextFactory,
Func<string, string, string> localize)
: this()
{
_componentLibraryService = componentLibraryService ?? throw new ArgumentNullException(nameof(componentLibraryService));
_createContextFactory = createContextFactory ?? throw new ArgumentNullException(nameof(createContextFactory));
_localize = localize ?? throw new ArgumentNullException(nameof(localize));
Reload();
}
public event EventHandler<string>? AddComponentRequested;
public void Reload()
{
if (_componentLibraryService is null ||
_createContextFactory is null ||
_localize is null)
{
return;
}
_viewModel.Title = _localize("component_library.title", "Widgets");
_viewModel.Categories.Clear();
_viewModel.Components.Clear();
var categories = _componentLibraryService.GetDesktopCategories();
foreach (var category in categories)
{
var itemModels = category.Components
.Select(CreateComponentItem)
.ToArray();
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
category.Id,
GetLocalizedCategoryTitle(category.Id),
ResolveCategoryIcon(category.Id),
itemModels));
}
if (_viewModel.Categories.Count == 0)
{
return;
}
if (CategoryListBox is not null)
{
CategoryListBox.SelectedIndex = 0;
}
}
private ComponentLibraryItemViewModel CreateComponentItem(ComponentLibraryComponentEntry entry)
{
if (_componentLibraryService is null ||
_createContextFactory is null ||
_localize is null)
{
return new ComponentLibraryItemViewModel(entry.ComponentId, entry.DisplayName, previewControl: null);
}
Control? previewControl = null;
_componentLibraryService.TryCreateControl(
entry.ComponentId,
_createContextFactory(42),
out previewControl,
out _);
if (previewControl is not null)
{
previewControl.IsHitTestVisible = false;
previewControl.Focusable = false;
}
return new ComponentLibraryItemViewModel(
entry.ComponentId,
string.IsNullOrWhiteSpace(entry.DisplayNameLocalizationKey)
? entry.DisplayName
: _localize(entry.DisplayNameLocalizationKey, entry.DisplayName),
previewControl);
}
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
_ = e;
_viewModel.Components.Clear();
if (CategoryListBox?.SelectedItem is not ComponentLibraryCategoryViewModel selectedCategory)
{
return;
}
foreach (var component in selectedCategory.Components)
{
_viewModel.Components.Add(component);
}
}
private void OnAddComponentClick(object? sender, RoutedEventArgs e)
{
_ = e;
if (sender is not Button button ||
button.Tag is not string componentId ||
string.IsNullOrWhiteSpace(componentId))
{
return;
}
AddComponentRequested?.Invoke(this, componentId);
}
private void OnCloseClick(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
Hide();
}
private Symbol ResolveCategoryIcon(string categoryId)
{
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase))
{
return Symbol.Clock;
}
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase))
{
return Symbol.CalendarDate;
}
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase))
{
return Symbol.WeatherSunny;
}
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase))
{
return Symbol.Edit;
}
if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase))
{
return Symbol.Play;
}
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
{
return Symbol.Apps;
}
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))
{
return Symbol.Calculator;
}
if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase))
{
return Symbol.Apps;
}
return Symbol.Apps;
}
private string GetLocalizedCategoryTitle(string categoryId)
{
if (_localize is null)
{
return categoryId;
}
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase))
{
return _localize("component_category.clock", "Clock");
}
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase))
{
return _localize("component_category.date", "Calendar");
}
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase))
{
return _localize("component_category.weather", "Weather");
}
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase))
{
return _localize("component_category.board", "Board");
}
if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase))
{
return _localize("component_category.media", "Media");
}
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
{
return _localize("component_category.info", "Info");
}
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))
{
return _localize("component_category.calculator", "Calculator");
}
if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase))
{
return _localize("component_category.study", "Study");
}
return categoryId;
}
}

View File

@@ -8,6 +8,8 @@ using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
@@ -59,7 +61,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
private string _componentId = BuiltInComponentIds.DesktopClock;
private string _placementId = string.Empty;
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private TimeZoneService? _timeZoneService;
private double _currentCellSize = 48;

View File

@@ -35,8 +35,8 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
Interval = TimeSpan.FromMinutes(15)
};
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly List<BaiduHotSearchItemSnapshot> _activeItems = [];
private readonly List<HotItemVisual> _hotItemVisuals = [];

View File

@@ -33,8 +33,8 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
Interval = TimeSpan.FromMinutes(15)
};
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly List<BilibiliHotSearchItemSnapshot> _activeItems = [];
private readonly List<HotItemVisual> _hotItemVisuals = [];

View File

@@ -336,8 +336,6 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
GoButton.IsEnabled = true;
AddressTextBox.IsEnabled = true;
UnavailableOverlay.IsVisible = false;
TryNavigate(_lastKnownUri, "Activate");
}
private void DeactivateWebView(bool clearUrl)

View File

@@ -9,6 +9,7 @@ using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
@@ -27,7 +28,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromMinutes(4)
};
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService();

View File

@@ -44,8 +44,8 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromMinutes(30)
};
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly Bitmap?[] _newsBitmaps = new Bitmap?[2];
private readonly List<string?> _newsUrls = [];
@@ -875,4 +875,3 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
return MultiWhitespaceRegex.Replace(text.Trim(), " ");
}
}

View File

@@ -15,6 +15,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
@@ -59,7 +60,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromHours(6)
};
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;

View File

@@ -55,7 +55,7 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
Interval = TimeSpan.FromHours(6)
};
private readonly AppSettingsService _settingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;

View File

@@ -33,8 +33,8 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromHours(6)
};
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;

View File

@@ -31,8 +31,8 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
Interval = TimeSpan.FromHours(6)
};
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.PluginSdk;
@@ -16,7 +17,9 @@ public sealed record DesktopComponentControlFactoryContext(
IWeatherInfoService WeatherInfoService,
IRecommendationInfoService RecommendationInfoService,
ICalculatorDataService CalculatorDataService,
ISettingsFacadeService SettingsFacade,
ISettingsService SettingsService,
IComponentInstanceSettingsStore ComponentSettingsStore,
IComponentSettingsAccessor ComponentSettingsAccessor,
string? PlacementId = null);
@@ -87,10 +90,15 @@ public sealed class DesktopComponentRuntimeDescriptor
IWeatherInfoService weatherInfoService,
IRecommendationInfoService recommendationInfoService,
ICalculatorDataService calculatorDataService,
ISettingsFacadeService settingsFacade,
string? placementId = null)
{
var settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
ArgumentNullException.ThrowIfNull(settingsFacade);
var settingsService = settingsFacade.Settings;
var componentAccessor = settingsService.GetComponentAccessor(Definition.Id, placementId);
var componentSettingsStore = new ComponentSettingsService(settingsService);
componentSettingsStore.SetScopedComponentContext(Definition.Id, placementId);
var control = _controlFactory(new DesktopComponentControlFactoryContext(
Definition,
cellSize,
@@ -98,20 +106,37 @@ public sealed class DesktopComponentRuntimeDescriptor
weatherInfoService,
recommendationInfoService,
calculatorDataService,
settingsFacade,
settingsService,
componentSettingsStore,
componentAccessor,
placementId));
var runtimeContext = new DesktopComponentRuntimeContext(
Definition.Id,
placementId,
settingsFacade,
settingsService,
componentAccessor);
componentAccessor,
componentSettingsStore);
ApplySettingsDependencies(control, settingsService, componentSettingsStore);
if (control is IComponentRuntimeContextAware runtimeContextAwareComponent)
{
runtimeContextAwareComponent.SetComponentRuntimeContext(runtimeContext);
}
if (control is IComponentSettingsContextAware settingsContextAwareComponent)
{
settingsContextAwareComponent.SetComponentSettingsContext(new DesktopComponentSettingsContext(
Definition.Id,
placementId,
settingsFacade,
settingsService,
componentAccessor,
componentSettingsStore));
}
if (control is IComponentPlacementContextAware placementAwareComponent)
{
placementAwareComponent.SetComponentPlacementContext(Definition.Id, placementId);
@@ -149,6 +174,57 @@ public sealed class DesktopComponentRuntimeDescriptor
{
return _cornerRadiusResolver(Math.Max(1, cellSize));
}
private static void ApplySettingsDependencies(
object? target,
ISettingsService settingsService,
IComponentInstanceSettingsStore componentSettingsStore)
{
if (target is null)
{
return;
}
var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
foreach (var field in target.GetType().GetFields(flags))
{
if (field.IsInitOnly)
{
continue;
}
if (typeof(ISettingsService).IsAssignableFrom(field.FieldType))
{
field.SetValue(target, settingsService);
continue;
}
if (typeof(IComponentInstanceSettingsStore).IsAssignableFrom(field.FieldType))
{
field.SetValue(target, componentSettingsStore);
}
}
foreach (var property in target.GetType().GetProperties(flags))
{
if (!property.CanWrite)
{
continue;
}
if (typeof(ISettingsService).IsAssignableFrom(property.PropertyType))
{
property.SetValue(target, settingsService);
continue;
}
if (typeof(IComponentInstanceSettingsStore).IsAssignableFrom(property.PropertyType))
{
property.SetValue(target, componentSettingsStore);
}
}
}
}
public sealed class DesktopComponentRuntimeRegistry
@@ -368,8 +444,11 @@ public sealed class DesktopComponentRuntimeRegistry
];
}
public static DesktopComponentRuntimeRegistry CreateDefault(ComponentRegistry componentRegistry)
public static DesktopComponentRuntimeRegistry CreateDefault(
ComponentRegistry componentRegistry,
ISettingsFacadeService settingsFacade)
{
_ = settingsFacade;
return new DesktopComponentRuntimeRegistry(componentRegistry, GetDefaultRegistrations());
}

View File

@@ -36,7 +36,7 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone
Interval = TimeSpan.FromMinutes(30)
};
private readonly AppSettingsService _settingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
private ICalculatorDataService _calculatorDataService = DefaultCalculatorService;

View File

@@ -11,6 +11,7 @@ using Avalonia.Media;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Theme;
@@ -26,7 +27,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
private readonly DispatcherTimer _animationTimer = new() { Interval = FluttermotionToken.WeatherAnimationFrameInterval };
private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1);
private readonly TranslateTransform _backgroundMotionTranslateTransform = new();
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService;

View File

@@ -95,8 +95,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
Interval = FluttermotionToken.WeatherAnimationFrameInterval
};
private readonly AppSettingsService _settingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();

View File

@@ -44,8 +44,8 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
Interval = TimeSpan.FromMinutes(20)
};
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly List<DailyNewsItemSnapshot> _activeItems = [];
private readonly List<NewsItemVisual> _itemVisuals = [];

View File

@@ -93,8 +93,8 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
Interval = FluttermotionToken.WeatherAnimationFrameInterval
};
private readonly AppSettingsService _settingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();

View File

@@ -29,7 +29,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
private readonly IMusicControlService _musicControlService = MusicControlServiceFactory.CreateDefault();
private readonly MonetColorService _monetColorService = new();
private readonly AppSettingsService _settingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private CancellationTokenSource? _refreshCts;

View File

@@ -26,7 +26,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
private readonly IAudioRecorderService _audioRecorderService = AudioRecorderServiceFactory.CreateRecorder();
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private readonly List<Border> _waveBars = [];
private readonly double[] _waveLevels = new double[WaveBarCount];

View File

@@ -45,8 +45,8 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
Interval = TimeSpan.FromMinutes(20)
};
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly List<Stcn24ForumPostItemSnapshot> _activeItems = [];
private readonly List<ForumItemVisual> _itemVisuals = [];

View File

@@ -41,7 +41,7 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
private static readonly Color LightSubstrate = Color.Parse("#FFF1F5FA");
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private readonly DispatcherTimer _uiTimer = new()
{
@@ -73,6 +73,7 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ApplyVariableFontFamily();
ReloadLanguageCode();
@@ -114,6 +115,11 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
ApplyTypographyByBackground(ResolvePanelBackgroundColor());
}
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
{
RefreshVisual();
}
private void OnUiTimerTick(object? sender, EventArgs e)
{
RefreshVisual();
@@ -572,7 +578,7 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
return solidBackground.Color;
}
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
resource is ISolidColorBrush solidBrush)
{
return solidBrush.Color;

View File

@@ -13,8 +13,8 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
{
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly DispatcherTimer _uiTimer = new()
{
@@ -38,6 +38,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ReloadDisplaySettings();
ApplyCellSize(_currentCellSize);
@@ -106,6 +107,11 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
UpdateAdaptiveLayout();
}
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
{
RefreshVisual();
}
private void OnUiTimerTick(object? sender, EventArgs e)
{
RefreshVisual();
@@ -330,7 +336,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
private IBrush TryResolveThemeBrush(string resourceKey, string fallbackHex)
{
if (Resources.TryGetResource(resourceKey, ActualThemeVariant, out var resource) && resource is IBrush brush)
if (this.TryFindResource(resourceKey, out var resource) && resource is IBrush brush)
{
return brush;
}
@@ -352,6 +358,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
AttachedToVisualTree -= OnAttachedToVisualTree;
DetachedFromVisualTree -= OnDetachedFromVisualTree;
SizeChanged -= OnSizeChanged;
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
_monitoringLease?.Dispose();
_monitoringLease = null;

View File

@@ -41,7 +41,7 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private readonly DispatcherTimer _uiTimer = new()
{
@@ -79,6 +79,7 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ApplyVariableFontFamily();
ReloadLanguageCode();
@@ -123,6 +124,11 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
ApplyTypographyByBackground(ResolvePanelBackgroundColor());
}
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
{
RefreshVisual();
}
private void OnUiTimerTick(object? sender, EventArgs e)
{
RefreshVisual();
@@ -506,7 +512,7 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
return solidBackground.Color;
}
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
resource is ISolidColorBrush solidBrush)
{
return solidBrush.Color;

View File

@@ -55,7 +55,7 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
private readonly object _snapshotSync = new();
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private readonly DispatcherTimer _renderTimer = new()
{
@@ -89,6 +89,7 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ReloadLanguageCode();
ApplyCellSize(_currentCellSize);
@@ -192,6 +193,24 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
ApplyTypographyByBackground(panelColor);
}
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
{
var panelColor = ResolvePanelBackgroundColor();
ApplyTypographyByBackground(panelColor);
ApplyStatusBadgeStyle(StatusVisualKind.Default, panelColor);
lock (_snapshotSync)
{
_pendingSnapshot = _studyAnalyticsService.GetSnapshot();
_hasPendingSnapshot = true;
}
if (!_renderTimer.IsEnabled)
{
OnRenderTimerTick(this, EventArgs.Empty);
}
}
private void OnStudySnapshotUpdated(object? sender, StudyAnalyticsSnapshotChangedEventArgs e)
{
lock (_snapshotSync)
@@ -375,7 +394,7 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
return solidBackground.Color;
}
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
resource is ISolidColorBrush solidBrush)
{
return solidBrush.Color;
@@ -580,6 +599,7 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
AttachedToVisualTree -= OnAttachedToVisualTree;
DetachedFromVisualTree -= OnDetachedFromVisualTree;
SizeChanged -= OnSizeChanged;
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
if (_isSubscribed)
{

View File

@@ -42,7 +42,7 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private readonly DispatcherTimer _uiTimer = new()
{
@@ -75,6 +75,7 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ApplyVariableFontFamily();
ReloadLanguageCode();
@@ -129,6 +130,11 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
ApplyTypographyByBackground(ResolvePanelBackgroundColor());
}
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
{
RefreshVisual();
}
private void OnUiTimerTick(object? sender, EventArgs e)
{
RefreshVisual();
@@ -396,7 +402,7 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
return solidBackground.Color;
}
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
resource is ISolidColorBrush solidBrush)
{
return solidBrush.Color;
@@ -627,10 +633,9 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
AttachedToVisualTree -= OnAttachedToVisualTree;
DetachedFromVisualTree -= OnDetachedFromVisualTree;
SizeChanged -= OnSizeChanged;
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
_monitoringLease?.Dispose();
_monitoringLease = null;
}
}

View File

@@ -42,7 +42,7 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private readonly DispatcherTimer _uiTimer = new()
{
@@ -68,6 +68,7 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ApplyVariableFontFamily();
ReloadLanguageCode();
@@ -112,6 +113,11 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
ApplyTypographyByBackground(ResolvePanelBackgroundColor());
}
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
{
RefreshVisual();
}
private void OnUiTimerTick(object? sender, EventArgs e)
{
RefreshVisual();
@@ -501,7 +507,7 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
return solidBackground.Color;
}
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
resource is ISolidColorBrush solidBrush)
{
return solidBrush.Color;

View File

@@ -50,7 +50,7 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private readonly DispatcherTimer _uiTimer = new()
{
@@ -76,6 +76,7 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ReloadLanguageCode();
ApplyCellSize(_currentCellSize);
@@ -119,6 +120,11 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
ApplyTypographyByBackground(ResolvePanelBackgroundColor());
}
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
{
RefreshVisual();
}
private void OnUiTimerTick(object? sender, EventArgs e)
{
RefreshVisual();
@@ -334,7 +340,7 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
return solidBackground.Color;
}
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
resource is ISolidColorBrush solidBrush)
{
return solidBrush.Color;
@@ -484,5 +490,6 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
AttachedToVisualTree -= OnAttachedToVisualTree;
DetachedFromVisualTree -= OnDetachedFromVisualTree;
SizeChanged -= OnSizeChanged;
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
}
}

View File

@@ -47,7 +47,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
private static readonly Color LightSubstrate = Color.Parse("#FFF1F5FA");
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
private readonly AppSettingsService _settingsService = new();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
private double _currentCellSize = 48;
@@ -72,6 +72,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
DialogCancelButton.Click += (_, _) => CloseDialog();
DialogConfirmButton.Click += (_, _) => ConfirmDialog();
DialogRenameTextBox.KeyDown += OnDialogRenameTextBoxKeyDown;
@@ -133,6 +134,14 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
}
}
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
{
if (_currentSnapshot is not null)
{
RenderSnapshot(_currentSnapshot);
}
}
private void OnStudySnapshotUpdated(object? sender, StudyAnalyticsSnapshotChangedEventArgs e)
{
Dispatcher.UIThread.Post(() =>
@@ -657,7 +666,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
return solidBackground.Color;
}
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
resource is ISolidColorBrush solidBrush)
{
return solidBrush.Color;
@@ -747,6 +756,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
AttachedToVisualTree -= OnAttachedToVisualTree;
DetachedFromVisualTree -= OnDetachedFromVisualTree;
SizeChanged -= OnSizeChanged;
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
DialogCancelButton.Click -= (_, _) => CloseDialog();
DialogConfirmButton.Click -= (_, _) => ConfirmDialog();
DialogRenameTextBox.KeyDown -= OnDialogRenameTextBoxKeyDown;
@@ -758,5 +768,3 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
}
}
}

View File

@@ -41,8 +41,8 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromMinutes(12)
};
private readonly AppSettingsService _settingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly Line _hourHandLine = CreateHandLine("#232938", 4.0);
private readonly Line _minuteHandLine = CreateHandLine("#2F3749", 2.8);

View File

@@ -89,8 +89,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
Interval = FluttermotionToken.WeatherAnimationFrameInterval
};
private readonly AppSettingsService _settingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();

View File

@@ -92,8 +92,8 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
Interval = TimeSpan.FromSeconds(1)
};
private readonly AppSettingsService _appSettingsService = new();
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly ClockEntryVisual[] _entryVisuals = new ClockEntryVisual[WorldClockTimeZoneCatalog.ClockCount];
private readonly TimeZoneInfo[] _entryTimeZones = new TimeZoneInfo[WorldClockTimeZoneCatalog.ClockCount];

View File

@@ -15,6 +15,7 @@ using FluentIcons.Common;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components;
@@ -44,7 +45,7 @@ public partial class MainWindow
private TranslateTransform? _componentLibraryCategoryHostTransform;
private TranslateTransform? _componentLibraryComponentHostTransform;
private IReadOnlyList<ComponentLibraryCategory> _componentLibraryCategories = Array.Empty<ComponentLibraryCategory>();
private IReadOnlyList<DesktopComponentRuntimeDescriptor> _componentLibraryActiveComponents = Array.Empty<DesktopComponentRuntimeDescriptor>();
private IReadOnlyList<ComponentLibraryComponentEntry> _componentLibraryActiveComponents = Array.Empty<ComponentLibraryComponentEntry>();
private bool _isComponentLibraryCategoryGestureActive;
private bool _isComponentLibraryComponentGestureActive;
private Point _componentLibraryCategoryGestureStartPoint;
@@ -95,13 +96,51 @@ public partial class MainWindow
string Id,
Symbol Icon,
string Title,
IReadOnlyList<DesktopComponentRuntimeDescriptor> Components);
IReadOnlyList<ComponentLibraryComponentEntry> Components);
private readonly record struct ComponentScaleRule(int WidthUnit, int HeightUnit, int MinScale);
private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e)
{
_componentLibraryWindowService.Toggle(this);
_ = sender;
_ = e;
if (_isComponentLibraryOpen)
{
CloseComponentLibraryWindow(reopenSettings: false);
return;
}
var settingsWindowService = (Application.Current as App)?.SettingsWindowService;
_reopenSettingsAfterComponentLibraryClose = settingsWindowService?.IsOpen == true;
if (_reopenSettingsAfterComponentLibraryClose)
{
settingsWindowService?.Close();
}
OpenComponentLibraryWindow();
}
private void OnOpenSettingsClick(object? sender, RoutedEventArgs e)
{
_ = sender;
_ = e;
if (_isComponentLibraryOpen)
{
CloseComponentLibraryWindow(reopenSettings: false);
}
var app = Application.Current as App;
if (app?.SettingsWindowService is { } settingsWindowService)
{
settingsWindowService.Toggle(new SettingsWindowOpenRequest(
Source: "MainWindowTaskbar",
Owner: this));
return;
}
app?.OpenIndependentSettingsModule("MainWindowTaskbar");
}
private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e)
@@ -220,16 +259,19 @@ public partial class MainWindow
private void ApplyTaskbarActionVisibility(TaskbarContext context)
{
if (BackToWindowsButton is null || OpenComponentLibraryButton is null)
if (BackToWindowsButton is null ||
OpenComponentLibraryButton is null ||
OpenSettingsButton is null)
{
return;
}
var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows);
var showSettings = false;
var showDesktopEdit = true;
var showSettings = true;
var showDesktopEdit = _isSettingsOpen;
BackToWindowsButton.IsVisible = showMinimize;
OpenSettingsButton.IsVisible = showSettings;
OpenComponentLibraryButton.IsVisible = showDesktopEdit;
if (TaskbarFixedActionsHost is not null)
@@ -242,6 +284,8 @@ public partial class MainWindow
TaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit;
}
UpdateOpenSettingsActionVisualState();
var dynamicActions = ResolveDynamicTaskbarActions(context)
.Where(action => action.IsVisible)
.ToList();
@@ -256,7 +300,25 @@ public partial class MainWindow
private void UpdateOpenSettingsActionVisualState()
{
// Open-settings action is removed in API-only settings mode.
if (OpenSettingsButtonTextBlock is null || OpenSettingsButton is null)
{
return;
}
var showBackToDesktop = _isSettingsOpen;
var buttonText = L("settings.back_to_desktop", "Back to Desktop");
OpenSettingsButtonTextBlock.IsVisible = showBackToDesktop;
OpenSettingsButtonTextBlock.Text = buttonText;
ToolTip.SetTip(
OpenSettingsButton,
showBackToDesktop
? buttonText
: L("tooltip.open_settings", "Settings"));
var effectiveCellSize = _currentDesktopCellSize > 0
? _currentDesktopCellSize
: Math.Max(32, Math.Min(Bounds.Width, Bounds.Height) / Math.Max(1, _targetShortSideCells));
ApplyWidgetSizing(effectiveCellSize);
}
private void OpenComponentLibraryWindow()
@@ -316,13 +378,109 @@ public partial class MainWindow
_reopenSettingsAfterComponentLibraryClose = false;
if (shouldReopenSettings)
{
AppLogger.Info(
"SettingsFacade",
"Reopen settings request ignored because settings UI entry is disabled during hard-cut migration.");
(Application.Current as App)?.OpenIndependentSettingsModule("ComponentLibraryClose");
}
}, FluttermotionToken.Slow);
}
private void OpenDetachedComponentLibraryWindow()
{
_detachedComponentLibraryWindow ??= CreateDetachedComponentLibraryWindow();
_detachedComponentLibraryWindow.Reload();
if (!_detachedComponentLibraryWindow.IsVisible)
{
if (IsVisible)
{
_detachedComponentLibraryWindow.Show(this);
}
else
{
_detachedComponentLibraryWindow.Show();
}
return;
}
_detachedComponentLibraryWindow.Activate();
}
private void CloseDetachedComponentLibraryWindow()
{
if (_detachedComponentLibraryWindow is null)
{
return;
}
_detachedComponentLibraryWindow.Hide();
}
private ComponentLibraryWindow CreateDetachedComponentLibraryWindow()
{
var window = new ComponentLibraryWindow(
_componentLibraryService,
cellSize => new ComponentLibraryCreateContext(
cellSize,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_settingsFacade),
L);
window.AddComponentRequested += OnDetachedComponentLibraryAddComponentRequested;
window.Closed += OnDetachedComponentLibraryClosed;
return window;
}
private void OnDetachedComponentLibraryAddComponentRequested(object? sender, string componentId)
{
_ = sender;
if (string.IsNullOrWhiteSpace(componentId) ||
_currentDesktopSurfaceIndex < 0 ||
_currentDesktopSurfaceIndex >= _desktopPageCount ||
!_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid) ||
!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var descriptor))
{
return;
}
var span = NormalizeComponentCellSpan(
componentId,
ComponentPlacementRules.EnsureMinimumSize(
descriptor.Definition,
descriptor.Definition.MinWidthCells,
descriptor.Definition.MinHeightCells));
var row = Math.Max(0, (pageGrid.RowDefinitions.Count - span.HeightCells) / 2);
var column = Math.Max(0, (pageGrid.ColumnDefinitions.Count - span.WidthCells) / 2);
PlaceDesktopComponentOnPage(componentId, _currentDesktopSurfaceIndex, row, column);
}
private void OnDetachedComponentLibraryClosed(object? sender, EventArgs e)
{
_ = e;
if (ReferenceEquals(sender, _detachedComponentLibraryWindow))
{
_detachedComponentLibraryWindow.AddComponentRequested -= OnDetachedComponentLibraryAddComponentRequested;
_detachedComponentLibraryWindow.Closed -= OnDetachedComponentLibraryClosed;
_detachedComponentLibraryWindow = null;
}
}
private void OnSettingsWindowStateChanged(object? sender, EventArgs e)
{
_ = sender;
_ = e;
SyncSettingsWindowState();
}
private void SyncSettingsWindowState()
{
var isOpen = (Application.Current as App)?.SettingsWindowService?.IsOpen == true;
_isSettingsOpen = isOpen;
UpdateDesktopPageAwareComponentContext();
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
UpdateOpenSettingsActionVisualState();
}
private void InitializeDesktopComponentDragHandlers()
{
// Global handlers: we capture the pointer during drag, then track move/release anywhere.
@@ -1245,6 +1403,21 @@ public partial class MainWindow
return CreateDesktopComponentControl(runtimeDescriptor, _currentDesktopCellSize, placementId, pageIndex, "DesktopSurface");
}
private Control? CreateDesktopComponentControl(
string componentId,
double cellSize,
string? placementId,
int? pageIndex,
string action)
{
if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
{
return null;
}
return CreateDesktopComponentControl(runtimeDescriptor, cellSize, placementId, pageIndex, action);
}
private Control? CreateDesktopComponentControl(
DesktopComponentRuntimeDescriptor runtimeDescriptor,
double cellSize,
@@ -1260,6 +1433,7 @@ public partial class MainWindow
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_settingsFacade,
placementId);
if (!_componentLibraryService.TryCreateControl(runtimeDescriptor.Definition.Id, createContext, out var component, out var exception) ||
component is null)
@@ -1291,6 +1465,7 @@ public partial class MainWindow
}
internal bool IsComponentLibraryOpenFromService => _isComponentLibraryOpen;
internal bool IsDetachedComponentLibraryWindowOpenFromService => _detachedComponentLibraryWindow is { IsVisible: true };
internal void OpenComponentLibraryWindowFromService()
{
@@ -1302,6 +1477,44 @@ public partial class MainWindow
CloseComponentLibraryWindow(reopenSettings: false);
}
internal void OpenDetachedComponentLibraryWindowFromService()
{
OpenDetachedComponentLibraryWindow();
}
internal void CloseDetachedComponentLibraryWindowFromService()
{
CloseDetachedComponentLibraryWindow();
}
public bool TryGetSettingsWindowAnchorBounds(out PixelRect anchorBounds)
{
anchorBounds = default;
if (!IsVisible || BottomTaskbarContainer is null)
{
return false;
}
var origin = BottomTaskbarContainer.TranslatePoint(new Point(0, 0), this);
if (origin is null)
{
return false;
}
var scale = RenderScaling > 0 ? RenderScaling : 1d;
var width = (int)Math.Round(BottomTaskbarContainer.Bounds.Width * scale);
var height = (int)Math.Round(BottomTaskbarContainer.Bounds.Height * scale);
if (width <= 0 || height <= 0)
{
return false;
}
var x = Position.X + (int)Math.Round(origin.Value.X * scale);
var y = Position.Y + (int)Math.Round(origin.Value.Y * scale);
anchorBounds = new PixelRect(x, y, width, height);
return true;
}
private void CollapseComponentLibraryPanel()
{
// Animate component library panel collapsing downward
@@ -1314,6 +1527,7 @@ public partial class MainWindow
_isComponentLibraryOpen = false;
CancelDesktopComponentDrag();
CancelDesktopComponentResize(restoreOriginalSpan: true);
CloseDetachedComponentLibraryWindow();
ClearDesktopComponentSelection();
ClearSelectedLauncherTile(refreshTaskbar: false);
UpdateDesktopComponentHostEditState();
@@ -2170,27 +2384,18 @@ public partial class MainWindow
private IReadOnlyList<ComponentLibraryCategory> GetComponentLibraryCategories()
{
var descriptors = _componentRuntimeRegistry.GetDesktopComponents();
if (descriptors.Count == 0)
var categories = _componentLibraryService.GetDesktopCategories();
if (categories.Count == 0)
{
return Array.Empty<ComponentLibraryCategory>();
}
return descriptors
.GroupBy(descriptor => descriptor.Definition.Category, StringComparer.OrdinalIgnoreCase)
.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
.Select(group =>
{
var categoryId = string.IsNullOrWhiteSpace(group.Key) ? "Other" : group.Key.Trim();
var components = group
.OrderBy(descriptor => descriptor.Definition.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList();
return new ComponentLibraryCategory(
categoryId,
ResolveComponentLibraryCategoryIcon(categoryId),
GetLocalizedComponentLibraryCategoryTitle(categoryId),
components);
})
return categories
.Select(category => new ComponentLibraryCategory(
category.Id,
ResolveComponentLibraryCategoryIcon(category.Id),
GetLocalizedComponentLibraryCategoryTitle(category.Id),
category.Components))
.ToList();
}
@@ -2411,12 +2616,7 @@ public partial class MainWindow
for (var i = 0; i < componentCount; i++)
{
var descriptor = _componentLibraryActiveComponents[i];
var definition = descriptor.Definition;
if (!definition.AllowDesktopPlacement)
{
continue;
}
var component = _componentLibraryActiveComponents[i];
var page = new Grid
{
@@ -2429,8 +2629,8 @@ public partial class MainWindow
var previewMaxWidth = _componentLibraryComponentPageWidth * 0.94;
var previewMaxHeight = viewportHeight * 0.86;
var previewSpan = NormalizeComponentCellSpan(
definition.Id,
(definition.MinWidthCells, definition.MinHeightCells));
component.ComponentId,
(component.MinWidthCells, component.MinHeightCells));
var previewCellSize = Math.Min(
previewMaxWidth / Math.Max(1, previewSpan.WidthCells),
previewMaxHeight / Math.Max(1, previewSpan.HeightCells));
@@ -2441,7 +2641,7 @@ public partial class MainWindow
var renderCellSize = Math.Clamp(previewCellSize * 1.15, 26, 110);
var previewControl = CreateDesktopComponentControl(
descriptor,
component.ComponentId,
renderCellSize,
placementId: null,
pageIndex: null,
@@ -2479,13 +2679,13 @@ public partial class MainWindow
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Child = previewViewbox,
Tag = definition.Id
Tag = component.ComponentId
};
previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed;
var label = new TextBlock
{
Text = GetLocalizedComponentDisplayName(descriptor),
Text = GetLocalizedComponentDisplayName(component),
FontSize = 14,
FontWeight = FontWeight.SemiBold,
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"),
@@ -2544,11 +2744,11 @@ public partial class MainWindow
ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear();
}
private string GetLocalizedComponentDisplayName(DesktopComponentRuntimeDescriptor descriptor)
private string GetLocalizedComponentDisplayName(ComponentLibraryComponentEntry component)
{
return string.IsNullOrWhiteSpace(descriptor.DisplayNameLocalizationKey)
? descriptor.Definition.DisplayName
: L(descriptor.DisplayNameLocalizationKey, descriptor.Definition.DisplayName);
return string.IsNullOrWhiteSpace(component.DisplayNameLocalizationKey)
? component.DisplayName
: L(component.DisplayNameLocalizationKey, component.DisplayName);
}
private void OnComponentLibraryComponentPreviewPointerPressed(object? sender, PointerPressedEventArgs e)

View File

@@ -2,10 +2,12 @@ using System;
using System.Globalization;
using System.IO;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Models;
@@ -276,6 +278,13 @@ public partial class MainWindow
private void ApplyNightModeState(bool enabled, bool refreshPalettes)
{
_isNightMode = enabled;
var requestedThemeVariant = enabled ? ThemeVariant.Dark : ThemeVariant.Light;
RequestedThemeVariant = requestedThemeVariant;
if (Application.Current is not null)
{
Application.Current.RequestedThemeVariant = requestedThemeVariant;
}
if (!refreshPalettes)
{
return;
@@ -335,13 +344,44 @@ public partial class MainWindow
_suppressSettingsPersistence = true;
try
{
InitializeLocalization(snapshot.LanguageCode);
if (string.IsNullOrWhiteSpace(snapshot.TimeZoneId))
{
_timeZoneService.CurrentTimeZone = TimeZoneInfo.Local;
}
else
{
_timeZoneService.SetTimeZoneById(snapshot.TimeZoneId);
}
_targetShortSideCells = Math.Clamp(
snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(),
MinShortSideCells,
MaxShortSideCells);
_gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset(snapshot.GridSpacingPreset);
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode);
_statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30);
ApplyTaskbarSettings(snapshot);
InitializeWeatherSettings(snapshot);
InitializeAutoStartWithWindowsSetting(snapshot);
InitializeAppRenderModeSetting(snapshot);
InitializeUpdateSettings(snapshot);
InitializeDesktopSurfaceState(layoutSnapshot);
InitializeLauncherVisibilitySettings(launcherSnapshot);
InitializeDesktopComponentPlacements(layoutSnapshot);
TryRestoreWallpaper(snapshot.WallpaperPath);
if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor))
{
_selectedThemeColor = savedThemeColor;
}
_isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold);
ApplyNightModeState(_isNightMode, refreshPalettes: true);
ApplyWallpaperBrush();
UpdateWallpaperDisplay();
InitializeTimeZoneSettings();
ApplyLocalization();
RebuildDesktopGrid();
}
finally

View File

@@ -285,9 +285,9 @@
Grid.Column="2"
Background="Transparent"
BorderThickness="0">
<Grid ColumnDefinitions="Auto,Auto"
<Grid ColumnDefinitions="Auto,Auto,Auto"
ColumnSpacing="8">
<Button x:Name="OpenComponentLibraryButton"
<Button x:Name="OpenSettingsButton"
Grid.Column="0"
IsVisible="False"
Padding="8"
@@ -296,6 +296,31 @@
Background="Transparent"
BorderThickness="0"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Click="OnOpenSettingsClick"
ToolTip.Tip="Settings">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<fi:FluentIcon x:Name="OpenSettingsIcon"
Icon="Settings"
IconVariant="Regular" />
<TextBlock x:Name="OpenSettingsButtonTextBlock"
IsVisible="False"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="Settings" />
</StackPanel>
</Button>
<Button x:Name="OpenComponentLibraryButton"
Grid.Column="1"
IsVisible="False"
Padding="8"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
BorderThickness="0"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Click="OnOpenComponentLibraryClick"
ToolTip.Tip="&#32452;&#20214;&#24211;">
<StackPanel Orientation="Horizontal"

View File

@@ -17,6 +17,7 @@ using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.VisualTree;
using FluentAvalonia.Styling;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
@@ -29,7 +30,7 @@ using LibVLCSharp.Shared;
namespace LanMountainDesktop.Views;
public partial class MainWindow : Window
public partial class MainWindow : Window, ISettingsWindowAnchorProvider
{
private enum WallpaperPlacement
{
@@ -84,7 +85,7 @@ public partial class MainWindow : Window
private readonly ISettingsService _settingsService;
private readonly IComponentLayoutStore _componentLayoutStore = ComponentDomainStorageProvider.Instance;
private readonly IComponentStateStore _componentStateStore = ComponentDomainStorageProvider.Instance;
private readonly IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
private readonly IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
private readonly TimeZoneService _timeZoneService;
private readonly WindowsStartupService _windowsStartupService = new();
@@ -94,7 +95,8 @@ public partial class MainWindow : Window
private readonly ComponentRegistry _componentRegistry;
private readonly DesktopComponentRuntimeRegistry _componentRuntimeRegistry;
private readonly IComponentLibraryService _componentLibraryService;
private readonly IComponentLibraryWindowService _componentLibraryWindowService = new ComponentLibraryWindowService();
private readonly IEmbeddedComponentLibraryService _componentLibraryWindowService = new EmbeddedComponentLibraryService();
private ComponentLibraryWindow? _detachedComponentLibraryWindow;
private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme;
private readonly HashSet<string> _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<TaskbarActionId> _pinnedTaskbarActions = [];
@@ -190,13 +192,19 @@ public partial class MainWindow : Window
InitializeComponent();
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
_componentRegistry,
pluginRuntimeService);
pluginRuntimeService,
_settingsFacade);
_componentLibraryService = new ComponentLibraryService(_componentRegistry, _componentRuntimeRegistry);
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
_settingsService.Changed += OnSettingsChanged;
PropertyChanged += OnWindowPropertyChanged;
InitializeDesktopSurfaceSwipeHandlers();
InitializeDesktopComponentDragHandlers();
if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService)
{
settingsWindowService.StateChanged += OnSettingsWindowStateChanged;
_isSettingsOpen = settingsWindowService.IsOpen;
}
}
private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e)
@@ -234,6 +242,7 @@ public partial class MainWindow : Window
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
SyncSettingsWindowState();
_suppressSettingsPersistence = true;
var snapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
@@ -295,6 +304,13 @@ public partial class MainWindow : Window
protected override void OnClosed(EventArgs e)
{
PersistSettings();
if (_detachedComponentLibraryWindow is not null)
{
_detachedComponentLibraryWindow.AddComponentRequested -= OnDetachedComponentLibraryAddComponentRequested;
_detachedComponentLibraryWindow.Closed -= OnDetachedComponentLibraryClosed;
_detachedComponentLibraryWindow.Close();
}
_detachedComponentLibraryWindow = null;
StopVideoWallpaper();
DisposeLauncherResources();
_videoWallpaperMedia?.Dispose();
@@ -316,6 +332,10 @@ public partial class MainWindow : Window
_settingsService.Changed -= OnSettingsChanged;
PropertyChanged -= OnWindowPropertyChanged;
DesktopHost.SizeChanged -= OnDesktopHostSizeChanged;
if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService)
{
settingsWindowService.StateChanged -= OnSettingsWindowStateChanged;
}
base.OnClosed(e);
}
@@ -985,6 +1005,17 @@ public partial class MainWindow : Window
BackToWindowsIcon.FontSize = taskbarIconSize;
BackToWindowsTextBlock.FontSize = taskbarTextSize;
SetButtonContentSpacing(BackToWindowsButton, buttonContentSpacing);
OpenSettingsButton.Margin = new Thickness(0);
OpenSettingsButton.Padding = taskbarButtonPadding;
OpenSettingsButton.FontSize = taskbarTextSize;
OpenSettingsButton.MinHeight = taskbarCellHeight;
OpenSettingsButton.MinWidth = OpenSettingsButtonTextBlock.IsVisible
? Math.Clamp(taskbarCellHeight * 2.35, 100, 340)
: Math.Clamp(taskbarCellHeight * 1.10, 48, 88);
OpenSettingsIcon.FontSize = taskbarIconSize;
OpenSettingsButtonTextBlock.FontSize = taskbarTextSize;
SetButtonContentSpacing(OpenSettingsButton, buttonContentSpacing);
OpenComponentLibraryButton.Margin = new Thickness(0);
OpenComponentLibraryButton.Padding = taskbarButtonPadding;

View File

@@ -0,0 +1,80 @@
<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"
x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage"
x:DataType="vm:AboutSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<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:SettingsSectionCard.CardContent>
<StackPanel Spacing="14">
<StackPanel Classes="settings-item">
<TextBlock Classes="settings-item-label"
Text="{Binding VersionLabel}" />
<TextBlock Opacity="0.82"
Text="{Binding VersionText}" />
</StackPanel>
<StackPanel Classes="settings-item">
<TextBlock Classes="settings-item-label"
Text="{Binding RenderBackendLabel}" />
<TextBlock Opacity="0.82"
Text="{Binding RenderBackendText}" />
</StackPanel>
</StackPanel>
</controls:SettingsSectionCard.CardContent>
</controls:SettingsSectionCard>
<ui:SettingsExpander Classes="settings-expander-card"
Header="{Binding UpdateHeader}"
IsExpanded="True">
<StackPanel Spacing="12">
<controls:SettingsOptionCard IconKey="ArrowSync"
Title="{Binding AutoCheckUpdatesLabel}">
<controls:SettingsOptionCard.ActionContent>
<ToggleSwitch IsChecked="{Binding AutoCheckUpdates}" />
</controls:SettingsOptionCard.ActionContent>
</controls:SettingsOptionCard>
<controls:SettingsOptionCard IconKey="ArrowSync"
Title="{Binding IncludePrereleaseUpdatesLabel}">
<controls:SettingsOptionCard.ActionContent>
<ToggleSwitch IsChecked="{Binding IncludePrereleaseUpdates}" />
</controls:SettingsOptionCard.ActionContent>
</controls:SettingsOptionCard>
<controls:SettingsOptionCard IconKey="ArrowSync"
Title="{Binding UpdateChannelLabel}">
<controls:SettingsOptionCard.DetailsContent>
<ComboBox ItemsSource="{Binding UpdateChannels}"
SelectedItem="{Binding SelectedUpdateChannel}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:SettingsOptionCard.DetailsContent>
</controls:SettingsOptionCard>
<StackPanel Orientation="Horizontal"
Spacing="12">
<Button Command="{Binding CheckForUpdatesCommand}"
Content="{Binding CheckForUpdatesButtonText}" />
<TextBlock VerticalAlignment="Center"
Opacity="0.76"
Text="{Binding UpdateStatus}" />
</StackPanel>
</StackPanel>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,30 @@
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
namespace LanMountainDesktop.Views.SettingsPages;
[SettingsPageInfo(
"about",
"About",
SettingsPageCategory.About,
IconKey = "Info",
SortOrder = 40,
TitleLocalizationKey = "settings.about.title",
DescriptionLocalizationKey = "settings.about.description")]
public partial class AboutSettingsPage : SettingsPageBase
{
public AboutSettingsPage()
: this(new AboutSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
{
}
public AboutSettingsPage(AboutSettingsPageViewModel viewModel)
{
ViewModel = viewModel;
DataContext = ViewModel;
InitializeComponent();
}
public AboutSettingsPageViewModel ViewModel { get; }
}

View File

@@ -0,0 +1,97 @@
<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"
x:Class="LanMountainDesktop.Views.SettingsPages.AppearanceSettingsPage"
x:DataType="vm:AppearanceSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<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"
Title="{Binding NightModeLabel}">
<controls:SettingsOptionCard.ActionContent>
<ToggleSwitch IsChecked="{Binding IsNightMode}" />
</controls:SettingsOptionCard.ActionContent>
</controls:SettingsOptionCard>
<controls:SettingsOptionCard IconKey="DesignIdeas"
Title="{Binding UseSystemChromeLabel}">
<controls:SettingsOptionCard.ActionContent>
<ToggleSwitch IsChecked="{Binding UseSystemChrome}" />
</controls:SettingsOptionCard.ActionContent>
</controls:SettingsOptionCard>
<controls:SettingsOptionCard IconKey="DesignIdeas"
Title="{Binding ThemeColorLabel}">
<controls:SettingsOptionCard.DetailsContent>
<TextBox Watermark="#AABBCC"
Text="{Binding ThemeColor}" />
</controls:SettingsOptionCard.DetailsContent>
</controls:SettingsOptionCard>
<controls:SettingsSectionCard IconKey="DesignIdeas"
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">
<TextBlock Classes="settings-item-label"
Text="{Binding WallpaperPlacementLabel}" />
<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"
Click="OnBrowseWallpaperClick"
Content="{Binding ImportWallpaperButtonText}" />
</StackPanel>
</controls:SettingsSectionCard.CardContent>
</controls:SettingsSectionCard>
<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>
</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(
"appearance",
"Appearance",
SettingsPageCategory.Appearance,
IconKey = "DesignIdeas",
SortOrder = 10,
TitleLocalizationKey = "settings.appearance.title",
DescriptionLocalizationKey = "settings.appearance.description")]
public partial class AppearanceSettingsPage : SettingsPageBase
{
public AppearanceSettingsPage()
: this(new AppearanceSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
{
}
public AppearanceSettingsPage(AppearanceSettingsPageViewModel viewModel)
{
ViewModel = viewModel;
DataContext = ViewModel;
InitializeComponent();
}
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

@@ -0,0 +1,50 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:ui="using:FluentAvalonia.UI.Controls"
x:Class="LanMountainDesktop.Views.SettingsPages.ComponentsSettingsPage"
x:DataType="vm:ComponentsSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<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}"
IsExpanded="True">
<StackPanel Spacing="14">
<StackPanel Classes="settings-item">
<TextBlock Classes="settings-item-label"
Text="{Binding ShortSideCellsLabel}" />
<NumericUpDown Minimum="6"
Maximum="96"
Value="{Binding ShortSideCells}" />
</StackPanel>
<StackPanel Classes="settings-item">
<TextBlock Classes="settings-item-label"
Text="{Binding EdgeInsetPercentLabel}" />
<NumericUpDown Minimum="0"
Maximum="30"
Value="{Binding EdgeInsetPercent}" />
</StackPanel>
<StackPanel Classes="settings-item">
<TextBlock Classes="settings-item-label"
Text="{Binding SpacingPresetLabel}" />
<ComboBox ItemsSource="{Binding SpacingPresets}"
SelectedItem="{Binding SelectedSpacingPreset}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</StackPanel>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,30 @@
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
namespace LanMountainDesktop.Views.SettingsPages;
[SettingsPageInfo(
"components",
"Components",
SettingsPageCategory.Components,
IconKey = "GridDots",
SortOrder = 20,
TitleLocalizationKey = "settings.components.title",
DescriptionLocalizationKey = "settings.components.description")]
public partial class ComponentsSettingsPage : SettingsPageBase
{
public ComponentsSettingsPage()
: this(new ComponentsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
{
}
public ComponentsSettingsPage(ComponentsSettingsPageViewModel viewModel)
{
ViewModel = viewModel;
DataContext = ViewModel;
InitializeComponent();
}
public ComponentsSettingsPageViewModel ViewModel { get; }
}

View File

@@ -0,0 +1,87 @@
<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"
x:Class="LanMountainDesktop.Views.SettingsPages.GeneralSettingsPage"
x:DataType="vm:GeneralSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<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:SettingsOptionCard.DetailsContent>
<ComboBox MinWidth="240"
ItemsSource="{Binding Languages}"
SelectedItem="{Binding SelectedLanguage}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:SettingsOptionCard.DetailsContent>
</controls:SettingsOptionCard>
<controls:SettingsOptionCard IconKey="Settings"
Title="{Binding TimeZoneHeader}"
Description="{Binding TimeZoneDescription}">
<controls:SettingsOptionCard.DetailsContent>
<ComboBox MinWidth="280"
ItemsSource="{Binding TimeZones}"
SelectedItem="{Binding SelectedTimeZone}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:TimeZoneOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:SettingsOptionCard.DetailsContent>
</controls:SettingsOptionCard>
<controls:SettingsSectionCard IconKey="Info"
Title="{Binding PreviewHeader}">
<controls:SettingsSectionCard.CardContent>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16"
RowDefinitions="Auto,Auto"
RowSpacing="12">
<TextBlock FontWeight="SemiBold"
Text="{Binding PreviewTimeLabel}" />
<TextBlock Grid.Column="1"
Opacity="0.82"
Text="{Binding PreviewTimeText}" />
<TextBlock Grid.Row="1"
FontWeight="SemiBold"
Text="{Binding PreviewDateLabel}" />
<TextBlock Grid.Row="1"
Grid.Column="1"
Opacity="0.82"
Text="{Binding PreviewDateText}" />
</Grid>
</controls:SettingsSectionCard.CardContent>
</controls:SettingsSectionCard>
<ui:SettingsExpander Classes="settings-expander-card"
Header="{Binding RuntimeHeader}"
Description="{Binding RuntimeDescription}"
IsExpanded="True">
<ui:SettingsExpander.Footer>
<ComboBox MinWidth="240"
ItemsSource="{Binding RenderModes}"
SelectedItem="{Binding SelectedRenderMode}">
<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,36 @@
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
namespace LanMountainDesktop.Views.SettingsPages;
[SettingsPageInfo(
"general",
"General",
SettingsPageCategory.General,
IconKey = "Settings",
SortOrder = 0,
TitleLocalizationKey = "settings.general.title",
DescriptionLocalizationKey = "settings.general.description")]
public partial class GeneralSettingsPage : SettingsPageBase
{
public GeneralSettingsPage()
: this(new GeneralSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
{
}
public GeneralSettingsPage(GeneralSettingsPageViewModel viewModel)
{
ViewModel = viewModel;
ViewModel.RestartRequested += OnRestartRequested;
DataContext = ViewModel;
InitializeComponent();
}
public GeneralSettingsPageViewModel ViewModel { get; }
private void OnRestartRequested()
{
RequestRestart(ViewModel.RenderModeRestartMessage);
}
}

View File

@@ -0,0 +1,18 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
x:Class="LanMountainDesktop.Views.SettingsPages.GeneratedPluginSettingsPage"
x:DataType="vm:PluginGeneratedSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container">
<TextBlock Classes="settings-section-title"
Text="{Binding Title}" />
<TextBlock x:Name="DescriptionTextBlock"
Classes="settings-section-description"
Text="{Binding Description}" />
<StackPanel x:Name="DynamicOptionsHost"
Spacing="0" />
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using LanMountainDesktop.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class GeneratedPluginSettingsPage : SettingsPageBase
{
public GeneratedPluginSettingsPage()
: this(
new PluginGeneratedSettingsPageViewModel(
HostSettingsFacadeProvider.GetOrCreate().Settings,
string.Empty,
new PluginSettingsSectionRegistration("_preview", "preview", []),
new PluginLocalizer(AppContext.BaseDirectory, "en-US")))
{
}
public GeneratedPluginSettingsPage(PluginGeneratedSettingsPageViewModel viewModel)
{
ViewModel = viewModel;
DataContext = ViewModel;
InitializeComponent();
if (DescriptionTextBlock is not null)
{
DescriptionTextBlock.IsVisible = !string.IsNullOrWhiteSpace(ViewModel.Description);
}
BuildDynamicOptions();
}
public PluginGeneratedSettingsPageViewModel ViewModel { get; }
private void BuildDynamicOptions()
{
if (DynamicOptionsHost is null)
{
return;
}
DynamicOptionsHost.Children.Clear();
foreach (var option in ViewModel.Section.Options)
{
DynamicOptionsHost.Children.Add(CreateOptionControl(option));
}
}
private Control CreateOptionControl(SettingsOptionDefinition option)
{
var title = ViewModel.Localizer.GetString(option.TitleLocalizationKey, option.TitleLocalizationKey);
var description = string.IsNullOrWhiteSpace(option.DescriptionLocalizationKey)
? null
: ViewModel.Localizer.GetString(option.DescriptionLocalizationKey, option.DescriptionLocalizationKey);
var card = new SettingsOptionCard
{
IconKey = "Settings",
Title = title,
Description = description
};
switch (option.OptionType)
{
case SettingsOptionType.Toggle:
card.ActionContent = CreateToggle(option);
break;
case SettingsOptionType.Number:
card.DetailsContent = CreateNumber(option);
break;
case SettingsOptionType.Select:
card.DetailsContent = CreateSelect(option);
break;
case SettingsOptionType.Path:
card.DetailsContent = CreateText(option, "Path");
break;
case SettingsOptionType.List:
card.DetailsContent = CreateText(option, "Comma-separated values");
break;
default:
card.DetailsContent = CreateText(option, null);
break;
}
return card;
}
private Control CreateToggle(SettingsOptionDefinition option)
{
var toggleSwitch = new ToggleSwitch
{
IsChecked = ViewModel.SettingsService.GetValue<bool?>(
SettingsScope.Plugin,
option.Key,
ViewModel.PluginId,
sectionId: ViewModel.Section.Id) ?? (option.DefaultValue as bool? ?? false)
};
toggleSwitch.IsCheckedChanged += (_, _) =>
{
ViewModel.SettingsService.SetValue(
SettingsScope.Plugin,
option.Key,
toggleSwitch.IsChecked == true,
ViewModel.PluginId,
sectionId: ViewModel.Section.Id,
changedKeys: [option.Key]);
};
return toggleSwitch;
}
private Control CreateNumber(SettingsOptionDefinition option)
{
var currentValue = ViewModel.SettingsService.GetValue<double?>(
SettingsScope.Plugin,
option.Key,
ViewModel.PluginId,
sectionId: ViewModel.Section.Id);
var numeric = new NumericUpDown
{
Minimum = (decimal)(option.Minimum ?? 0d),
Maximum = (decimal)(option.Maximum ?? 9999d),
Value = (decimal)(currentValue ?? Convert.ToDouble(option.DefaultValue ?? 0d))
};
numeric.ValueChanged += (_, _) =>
{
ViewModel.SettingsService.SetValue(
SettingsScope.Plugin,
option.Key,
(double)(numeric.Value ?? 0m),
ViewModel.PluginId,
sectionId: ViewModel.Section.Id,
changedKeys: [option.Key]);
};
return numeric;
}
private Control CreateSelect(SettingsOptionDefinition option)
{
var choices = option.Choices
.Select(choice => new SelectionOption(
choice.Value,
ViewModel.Localizer.GetString(choice.TitleLocalizationKey, choice.TitleLocalizationKey)))
.ToArray();
var comboBox = new ComboBox
{
ItemsSource = choices
};
var currentValue = ViewModel.SettingsService.GetValue<string>(
SettingsScope.Plugin,
option.Key,
ViewModel.PluginId,
sectionId: ViewModel.Section.Id);
comboBox.SelectedItem = choices.FirstOrDefault(choice =>
string.Equals(choice.Value, currentValue ?? option.DefaultValue?.ToString(), StringComparison.OrdinalIgnoreCase));
comboBox.SelectionChanged += (_, _) =>
{
if (comboBox.SelectedItem is not SelectionOption selected)
{
return;
}
ViewModel.SettingsService.SetValue(
SettingsScope.Plugin,
option.Key,
selected.Value,
ViewModel.PluginId,
sectionId: ViewModel.Section.Id,
changedKeys: [option.Key]);
};
return comboBox;
}
private Control CreateText(SettingsOptionDefinition option, string? watermark)
{
var currentValue = option.OptionType == SettingsOptionType.List
? string.Join(
", ",
ViewModel.SettingsService.GetValue<IReadOnlyList<string>>(
SettingsScope.Plugin,
option.Key,
ViewModel.PluginId,
sectionId: ViewModel.Section.Id) ?? (option.DefaultValue as IReadOnlyList<string> ?? []))
: ViewModel.SettingsService.GetValue<string>(
SettingsScope.Plugin,
option.Key,
ViewModel.PluginId,
sectionId: ViewModel.Section.Id) ?? option.DefaultValue?.ToString() ?? string.Empty;
var textBox = new TextBox
{
Watermark = watermark,
Text = currentValue
};
textBox.LostFocus += (_, _) =>
{
object value = option.OptionType == SettingsOptionType.List
? textBox.Text?
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToArray() ?? []
: textBox.Text ?? string.Empty;
ViewModel.SettingsService.SetValue(
SettingsScope.Plugin,
option.Key,
value,
ViewModel.PluginId,
sectionId: ViewModel.Section.Id,
changedKeys: [option.Key]);
};
return textBox;
}
}

View File

@@ -0,0 +1,87 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
x:Class="LanMountainDesktop.Views.SettingsPages.PluginsSettingsPage"
x:Name="Root"
x:DataType="vm:PluginsSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<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"
Margin="0,0,0,18">
<Button Command="{Binding RefreshCommand}"
Content="{Binding RefreshButtonText}" />
<TextBlock VerticalAlignment="Center"
Opacity="0.76"
Text="{Binding StatusMessage}" />
</StackPanel>
<TextBlock Classes="settings-subsection-title"
Text="{Binding InstalledHeader}" />
<ItemsControl ItemsSource="{Binding InstalledPlugins}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:InstalledPluginItemViewModel">
<Border Classes="settings-list-item">
<Grid Classes="settings-list-actions"
ColumnDefinitions="*,Auto,Auto">
<StackPanel>
<TextBlock FontWeight="SemiBold"
Text="{Binding Name}" />
<TextBlock Opacity="0.76"
FontSize="12"
Text="{Binding Description}" />
<TextBlock Opacity="0.58"
FontSize="11"
Text="{Binding Version}" />
</StackPanel>
<ToggleSwitch Grid.Column="1"
IsChecked="{Binding IsEnabled}"
Command="{Binding #Root.DataContext.TogglePluginCommand}"
CommandParameter="{Binding}" />
<Button Grid.Column="2"
Command="{Binding #Root.DataContext.DeletePluginCommand}"
CommandParameter="{Binding}"
Content="{Binding #Root.DataContext.DeleteButtonText}" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Classes="settings-subsection-title"
Text="{Binding MarketplaceHeader}" />
<ItemsControl ItemsSource="{Binding MarketPlugins}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:PluginMarketItemViewModel">
<Border Classes="settings-list-item">
<Grid Classes="settings-list-actions"
ColumnDefinitions="*,Auto">
<StackPanel>
<TextBlock FontWeight="SemiBold"
Text="{Binding Name}" />
<TextBlock Opacity="0.76"
FontSize="12"
Text="{Binding Description}" />
<TextBlock Opacity="0.58"
FontSize="11"
Text="{Binding Version}" />
</StackPanel>
<Button Grid.Column="1"
Command="{Binding #Root.DataContext.InstallPluginCommand}"
CommandParameter="{Binding}"
Content="{Binding #Root.DataContext.InstallButtonText}" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,41 @@
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
namespace LanMountainDesktop.Views.SettingsPages;
[SettingsPageInfo(
"plugins",
"Plugins",
SettingsPageCategory.Plugins,
IconKey = "PuzzlePiece",
SortOrder = 30,
TitleLocalizationKey = "settings.plugins.title",
DescriptionLocalizationKey = "settings.plugins.description")]
public partial class PluginsSettingsPage : SettingsPageBase
{
public PluginsSettingsPage()
: this(new PluginsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
{
}
public PluginsSettingsPage(PluginsSettingsPageViewModel viewModel)
{
ViewModel = viewModel;
ViewModel.RestartRequested += OnRestartRequested;
DataContext = ViewModel;
InitializeComponent();
}
public PluginsSettingsPageViewModel ViewModel { get; }
public override async void OnNavigatedTo(object? parameter)
{
await ViewModel.InitializeAsync();
}
private void OnRestartRequested()
{
RequestRestart(ViewModel.RestartRequiredMessage);
}
}

View File

@@ -0,0 +1,165 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia"
x:Class="LanMountainDesktop.Views.SettingsWindow"
x:DataType="vm:SettingsWindowViewModel"
Width="1120"
Height="760"
MinWidth="920"
MinHeight="620"
CanResize="True"
WindowStartupLocation="Manual"
SystemDecorations="BorderOnly"
FontFamily="{DynamicResource AppFontFamily}"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
Icon="/Assets/avalonia-logo.ico"
Title="{Binding Title}">
<Grid Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
RowDefinitions="Auto,Auto,*">
<Border x:Name="WindowTitleBarHost"
Height="48"
Padding="12,0,12,0"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="0,0,0,1"
PointerPressed="OnWindowTitleBarPointerPressed">
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto"
ColumnSpacing="8"
VerticalAlignment="Center">
<Button x:Name="TogglePaneButton"
Width="40"
Height="32"
Padding="0"
Background="Transparent"
BorderThickness="0"
Click="OnTogglePaneButtonClick">
<fi:FluentIcon x:Name="TogglePaneButtonIcon"
Icon="PanelLeftExpand"
IconVariant="Regular" />
</Button>
<fi:FluentIcon x:Name="WindowBrandIcon"
Grid.Column="1"
Margin="6,0,2,0"
Icon="Settings"
IconVariant="Filled"
IsHitTestVisible="False"
VerticalAlignment="Center" />
<StackPanel Grid.Column="2"
Orientation="Horizontal"
VerticalAlignment="Center"
Spacing="10">
<TextBlock x:Name="WindowTitleTextBlock"
FontSize="12"
FontWeight="SemiBold"
IsHitTestVisible="False"
Text="{Binding Title}" />
</StackPanel>
<Button x:Name="RestartNowButton"
Grid.Column="3"
Padding="10,6"
Margin="0,0,4,0"
Background="Transparent"
IsVisible="{Binding IsRestartRequested}"
Click="OnRestartNowClick">
<StackPanel Orientation="Horizontal"
Spacing="6">
<fi:FluentIcon x:Name="RestartButtonIcon"
Icon="ArrowSync"
IconVariant="Regular" />
<TextBlock x:Name="RestartButtonTextBlock"
Text="{Binding RestartButtonText}" />
</StackPanel>
</Button>
<Button x:Name="CloseWindowButton"
Grid.Column="4"
Width="40"
Height="32"
Padding="0"
Background="Transparent"
BorderThickness="0"
Click="OnCloseWindowClick">
<fi:FluentIcon x:Name="CloseWindowButtonIcon"
Icon="Dismiss"
IconVariant="Regular" />
</Button>
</Grid>
</Border>
<ui:InfoBar x:Name="RestartInfoBar"
Grid.Row="1"
IsOpen="{Binding IsRestartRequested}"
Margin="16,8,16,0"
Severity="Informational"
IsClosable="False"
Title="{Binding RestartTitle}"
Message="{Binding RestartMessage}">
<ui:InfoBar.ActionButton>
<Button Click="OnRestartNowClick"
Content="{Binding RestartButtonText}" />
</ui:InfoBar.ActionButton>
</ui:InfoBar>
<ui:NavigationView x:Name="RootNavigationView"
Grid.Row="2"
Margin="0,8,0,0"
Background="Transparent"
PaneDisplayMode="Auto"
OpenPaneLength="283"
IsSettingsVisible="False"
IsPaneToggleButtonVisible="False"
IsBackButtonVisible="False"
SelectionChanged="OnNavigationSelectionChanged">
<ui:NavigationView.Resources>
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
</ui:NavigationView.Resources>
<Grid ColumnDefinitions="*,Auto"
ColumnSpacing="20"
Margin="12,0,16,16">
<ui:Frame x:Name="ContentFrame" />
<Border x:Name="DrawerBorder"
Grid.Column="1"
Width="296"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
BorderThickness="1"
CornerRadius="16"
Padding="20"
BoxShadow="0 8 24 #14000000"
IsVisible="{Binding IsDrawerOpen}">
<Grid RowDefinitions="Auto,*"
RowSpacing="16">
<Grid ColumnDefinitions="*,Auto">
<TextBlock x:Name="DrawerTitleTextBlock"
FontSize="16"
FontWeight="SemiBold"
Text="{Binding DrawerTitle}" />
<Button Grid.Column="1"
Width="32"
Height="32"
Padding="0"
Background="Transparent"
BorderThickness="0"
Click="OnCloseDrawerClick">
<fi:FluentIcon Icon="Dismiss"
IconVariant="Regular" />
</Button>
</Grid>
<ContentControl x:Name="DrawerContentHost"
Grid.Row="1" />
</Grid>
</Border>
</Grid>
</ui:NavigationView>
</Grid>
</Window>

View File

@@ -0,0 +1,467 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
using Symbol = FluentIcons.Common.Symbol;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow : Window, ISettingsPageHostContext
{
private readonly ISettingsPageRegistry _pageRegistry;
private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
private readonly Dictionary<string, Control> _cachedPages = new(StringComparer.OrdinalIgnoreCase);
private readonly bool _useSystemChrome;
public SettingsWindow()
: this(
new SettingsWindowViewModel(),
EmptySettingsPageRegistry.Instance,
new HostApplicationLifecycleService())
{
}
public SettingsWindow(
SettingsWindowViewModel viewModel,
ISettingsPageRegistry pageRegistry,
IHostApplicationLifecycle hostApplicationLifecycle,
bool useSystemChrome = false)
{
_useSystemChrome = useSystemChrome;
ViewModel = viewModel;
_pageRegistry = pageRegistry;
_hostApplicationLifecycle = hostApplicationLifecycle;
DataContext = ViewModel;
InitializeComponent();
ApplyChromeMode(useSystemChrome);
Opened += OnOpened;
SizeChanged += OnWindowSizeChanged;
Closed += OnClosed;
Loaded += OnLoaded;
PendingRestartStateService.StateChanged += OnPendingRestartStateChanged;
}
public SettingsWindowViewModel ViewModel { get; }
private void OnLoaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
SyncPendingRestartState();
SyncTitleText();
UpdateChromeMetrics();
UpdatePaneToggleIcon();
}
public void ReloadPages(string? pageId)
{
ViewModel.Pages.Clear();
foreach (var page in _pageRegistry.GetPages().Where(page => !page.HideDefault))
{
ViewModel.Pages.Add(page);
}
_cachedPages.Clear();
CloseDrawer();
RebuildNavigationItems();
NavigateTo(pageId ?? ViewModel.Pages.FirstOrDefault()?.PageId);
}
public void OpenDrawer(Control content, string? title = null)
{
if (DrawerContentHost is null)
{
return;
}
DrawerContentHost.Content = content;
ViewModel.DrawerTitle = title ?? ViewModel.DrawerFallbackTitle;
ViewModel.IsDrawerOpen = true;
SyncTitleText();
}
public void CloseDrawer()
{
if (DrawerContentHost is not null)
{
DrawerContentHost.Content = null;
}
ViewModel.IsDrawerOpen = false;
ViewModel.DrawerTitle = null;
SyncTitleText();
}
public void RequestRestart(string? reason = null)
{
ViewModel.RestartMessage = string.IsNullOrWhiteSpace(reason)
? ViewModel.GetDefaultRestartMessage()
: reason;
ViewModel.IsRestartRequested = true;
}
public void ApplyChromeMode(bool useSystemChrome)
{
if (useSystemChrome || OperatingSystem.IsMacOS())
{
ExtendClientAreaToDecorationsHint = true;
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
ExtendClientAreaTitleBarHeightHint = -1;
SystemDecorations = SystemDecorations.Full;
return;
}
SystemDecorations = SystemDecorations.BorderOnly;
ExtendClientAreaToDecorationsHint = true;
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
ExtendClientAreaTitleBarHeightHint = 48;
}
public void RefreshShellText()
{
SyncPendingRestartState();
SyncTitleText();
}
private void RebuildNavigationItems()
{
if (RootNavigationView is null)
{
return;
}
RootNavigationView.MenuItems.Clear();
SettingsPageCategory? previousCategory = null;
foreach (var page in ViewModel.Pages)
{
if (previousCategory is not null && previousCategory != page.Category)
{
RootNavigationView.MenuItems.Add(new NavigationViewItemSeparator());
}
RootNavigationView.MenuItems.Add(new NavigationViewItem
{
Content = page.Title,
Tag = page.PageId,
IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = MapIcon(page.IconKey),
IconVariant = FluentIcons.Common.IconVariant.Regular
}
});
previousCategory = page.Category;
}
}
private void OnNavigationSelectionChanged(object? sender, NavigationViewSelectionChangedEventArgs e)
{
var selectedItem = e.SelectedItemContainer ?? e.SelectedItem as NavigationViewItem;
NavigateTo(selectedItem?.Tag as string);
}
private void NavigateTo(string? pageId)
{
var descriptor = ResolveDescriptor(pageId);
if (descriptor is null)
{
return;
}
var page = GetOrCreatePage(descriptor);
if (page is SettingsPageBase settingsPage)
{
settingsPage.InitializeHostContext(this);
settingsPage.NavigationUri = new Uri($"lmd://settings/{descriptor.PageId}", UriKind.Absolute);
settingsPage.OnNavigatedTo(null);
}
if (ContentFrame is not null)
{
ContentFrame.Content = page;
}
ViewModel.CurrentPageTitle = descriptor.Title;
ViewModel.CurrentPageDescription = descriptor.Description;
ViewModel.CurrentPageId = descriptor.PageId;
TrySelectNavigationItem(descriptor.PageId);
SyncTitleText();
}
private SettingsPageDescriptor? ResolveDescriptor(string? pageId)
{
if (!string.IsNullOrWhiteSpace(pageId) &&
_pageRegistry.TryGetPage(pageId, out var descriptor) &&
descriptor is not null)
{
return descriptor;
}
return ViewModel.Pages.FirstOrDefault();
}
private Control GetOrCreatePage(SettingsPageDescriptor descriptor)
{
if (_cachedPages.TryGetValue(descriptor.PageId, out var page))
{
return page;
}
page = descriptor.CreatePage(this);
if (page is SettingsPageBase settingsPage)
{
settingsPage.InitializeHostContext(this);
}
_cachedPages[descriptor.PageId] = page;
return page;
}
private void TrySelectNavigationItem(string pageId)
{
if (RootNavigationView is null)
{
return;
}
foreach (var item in RootNavigationView.MenuItems.OfType<NavigationViewItem>())
{
if (string.Equals(item.Tag as string, pageId, StringComparison.OrdinalIgnoreCase))
{
RootNavigationView.SelectedItem = item;
return;
}
}
}
private async void OnRestartNowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_ = sender;
_ = e;
_hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest(
Source: "SettingsWindow",
Reason: "User accepted restart from settings window."));
await Task.CompletedTask;
}
private void OnCloseDrawerClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_ = sender;
_ = e;
CloseDrawer();
}
private void OnPendingRestartStateChanged()
{
SyncPendingRestartState();
}
private void SyncPendingRestartState()
{
if (!PendingRestartStateService.HasPendingRestart && !ViewModel.IsRestartRequested)
{
return;
}
if (PendingRestartStateService.HasPendingRestart && string.IsNullOrWhiteSpace(ViewModel.RestartMessage))
{
ViewModel.RestartMessage = ViewModel.GetDefaultRestartMessage();
}
ViewModel.IsRestartRequested = ViewModel.IsRestartRequested || PendingRestartStateService.HasPendingRestart;
}
private void OnOpened(object? sender, EventArgs e)
{
_ = sender;
_ = e;
UpdateChromeMetrics();
}
private void OnWindowSizeChanged(object? sender, SizeChangedEventArgs e)
{
_ = sender;
_ = e;
UpdateChromeMetrics();
}
private void OnClosed(object? sender, EventArgs e)
{
_cachedPages.Clear();
PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged;
Opened -= OnOpened;
SizeChanged -= OnWindowSizeChanged;
}
private void OnWindowTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
{
_ = sender;
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
BeginMoveDrag(e);
}
}
private void OnTogglePaneButtonClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_ = sender;
_ = e;
if (RootNavigationView is null)
{
return;
}
RootNavigationView.IsPaneOpen = !RootNavigationView.IsPaneOpen;
UpdatePaneToggleIcon();
}
private void OnCloseWindowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_ = sender;
_ = e;
Close();
}
private void UpdatePaneToggleIcon()
{
if (TogglePaneButtonIcon is null || RootNavigationView is null)
{
return;
}
TogglePaneButtonIcon.Icon = RootNavigationView.IsPaneOpen
? FluentIcons.Common.Icon.PanelLeftContract
: FluentIcons.Common.Icon.PanelLeftExpand;
}
private void UpdateChromeMetrics()
{
if (_useSystemChrome)
{
if (WindowTitleBarHost is { })
{
WindowTitleBarHost.IsVisible = false;
}
return;
}
if (WindowTitleBarHost is null ||
TogglePaneButton is null ||
TogglePaneButtonIcon is null ||
WindowBrandIcon is null ||
WindowTitleTextBlock is null ||
RestartNowButton is null ||
RestartButtonIcon is null ||
RestartButtonTextBlock is null ||
CloseWindowButton is null ||
CloseWindowButtonIcon is null ||
DrawerTitleTextBlock is null ||
RootNavigationView is null)
{
return;
}
var width = Bounds.Width > 1 ? Bounds.Width : Math.Max(Width, MinWidth);
var height = Bounds.Height > 1 ? Bounds.Height : Math.Max(Height, MinHeight);
var layoutScale = Math.Clamp(Math.Min(width / 1120d, height / 760d), 0.90, 1.18);
var titleBarHeight = Math.Clamp(48d * layoutScale, 44d, 58d);
var titleBarButtonWidth = Math.Clamp(40d * layoutScale, 36d, 48d);
var titleBarButtonHeight = Math.Clamp(32d * layoutScale, 30d, 38d);
var titleFontSize = Math.Clamp(12d * layoutScale, 11d, 14d);
var titleBarIconSize = Math.Clamp(16d * layoutScale, 15d, 20d);
var drawerTitleFontSize = Math.Clamp(16d * layoutScale, 14d, 20d);
var chromePadding = Math.Clamp(12d * layoutScale, 10d, 18d);
var restartSpacing = Math.Clamp(6d * layoutScale, 6d, 10d);
ExtendClientAreaTitleBarHeightHint = titleBarHeight;
WindowTitleBarHost.Height = titleBarHeight;
WindowTitleBarHost.Padding = new Thickness(chromePadding, 0, chromePadding, 0);
TogglePaneButton.Width = titleBarButtonWidth;
TogglePaneButton.Height = titleBarButtonHeight;
TogglePaneButtonIcon.FontSize = titleBarIconSize;
WindowBrandIcon.FontSize = titleBarIconSize + 2;
WindowTitleTextBlock.FontSize = titleFontSize;
RestartNowButton.Padding = new Thickness(chromePadding * 0.9, Math.Max(6, chromePadding * 0.5));
if (RestartNowButton.Content is StackPanel restartStack)
{
restartStack.Spacing = restartSpacing;
}
RestartButtonIcon.FontSize = titleBarIconSize;
RestartButtonTextBlock.FontSize = titleFontSize;
CloseWindowButton.Width = titleBarButtonWidth;
CloseWindowButton.Height = titleBarButtonHeight;
CloseWindowButtonIcon.FontSize = titleBarIconSize;
DrawerTitleTextBlock.FontSize = drawerTitleFontSize;
RootNavigationView.OpenPaneLength = Math.Clamp(283d * layoutScale, 248d, 320d);
}
private void SyncTitleText()
{
Title = ViewModel.Title;
if (_useSystemChrome)
{
return;
}
if (WindowTitleTextBlock is null ||
DrawerTitleTextBlock is null)
{
return;
}
WindowTitleTextBlock.Text = ViewModel.Title;
DrawerTitleTextBlock.IsVisible = !string.IsNullOrWhiteSpace(ViewModel.DrawerTitle);
}
private sealed class EmptySettingsPageRegistry : ISettingsPageRegistry
{
public static EmptySettingsPageRegistry Instance { get; } = new();
public void Rebuild()
{
}
public IReadOnlyList<SettingsPageDescriptor> GetPages()
{
return Array.Empty<SettingsPageDescriptor>();
}
public bool TryGetPage(string pageId, out SettingsPageDescriptor? descriptor)
{
descriptor = null;
return false;
}
}
private static Symbol MapIcon(string iconKey)
{
return iconKey?.Trim() switch
{
"DesignIdeas" => Symbol.Color,
"GridDots" => Symbol.GridDots,
"PuzzlePiece" => Symbol.PuzzlePiece,
"Info" => Symbol.Info,
"ArrowSync" => Symbol.ArrowSync,
_ => Symbol.Settings
};
}
}

View File

@@ -11,6 +11,7 @@ using System.Threading;
using System.Threading.Tasks;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.PluginSdk;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -310,10 +311,15 @@ public sealed class PluginLoader
services.AddSingleton(runtimeContext.Manifest);
services.AddSingleton<IReadOnlyDictionary<string, object?>>(runtimeContext.Properties);
services.AddSingleton<IPluginMessageBus, PluginMessageBus>();
services.AddSingleton<IPluginSettingsService>(provider =>
new PluginScopedSettingsService(
runtimeContext.Manifest.Id,
provider.GetRequiredService<ISettingsService>()));
RegisterHostService<IPluginPackageManager>(services, hostServices);
RegisterHostService<IHostApplicationLifecycle>(services, hostServices);
RegisterHostService<IPluginExportRegistry>(services, hostServices);
RegisterHostService<ISettingsFacadeService>(services, hostServices);
RegisterHostService<ISettingsService>(services, hostServices);
RegisterHostService<ISettingsCatalog>(services, hostServices);

View File

@@ -55,6 +55,7 @@ public sealed class PluginRuntimeService : IDisposable
_packageManager,
_applicationLifecycle,
_exportRegistry,
_settingsFacade,
_settingsFacade.Settings,
_settingsFacade.Catalog);
_loaderOptions = CreateOptions();
@@ -824,6 +825,7 @@ public sealed class PluginRuntimeService : IDisposable
private readonly IPluginPackageManager _packageManager;
private readonly IHostApplicationLifecycle _applicationLifecycle;
private readonly IPluginExportRegistry _exportRegistry;
private readonly ISettingsFacadeService _settingsFacade;
private readonly ISettingsService _settingsService;
private readonly ISettingsCatalog _settingsCatalog;
@@ -831,12 +833,14 @@ public sealed class PluginRuntimeService : IDisposable
IPluginPackageManager packageManager,
IHostApplicationLifecycle applicationLifecycle,
IPluginExportRegistry exportRegistry,
ISettingsFacadeService settingsFacade,
ISettingsService settingsService,
ISettingsCatalog settingsCatalog)
{
_packageManager = packageManager;
_applicationLifecycle = applicationLifecycle;
_exportRegistry = exportRegistry;
_settingsFacade = settingsFacade;
_settingsService = settingsService;
_settingsCatalog = settingsCatalog;
}
@@ -858,6 +862,11 @@ public sealed class PluginRuntimeService : IDisposable
return _exportRegistry;
}
if (serviceType == typeof(ISettingsFacadeService))
{
return _settingsFacade;
}
if (serviceType == typeof(ISettingsService))
{
return _settingsService;