diff --git a/.trae/specs/settings-page-fluent-redesign/checklist.md b/.trae/specs/settings-page-fluent-redesign/checklist.md
new file mode 100644
index 0000000..109786b
--- /dev/null
+++ b/.trae/specs/settings-page-fluent-redesign/checklist.md
@@ -0,0 +1,32 @@
+# Checklist - 设置页面 Fluent 设计改造
+
+## Phase 1: 分析与准备
+
+- [ ] SettingsExpander 控件分析完成
+- [ ] 当前布局问题定位完成
+
+## Phase 2: 窗口布局调整
+
+- [ ] SettingsWindow 内容区域无额外 Border 包裹
+- [ ] 窗口整体视觉效果正常
+- [ ] 窗口圆角在不同模式下正确显示
+
+## Phase 3: 设置页面改造
+
+- [ ] AppearanceSettingsPage 无额外边框包裹
+- [ ] GeneralSettingsPage 无额外边框包裹
+- [ ] ComponentsSettingsPage 无额外边框包裹
+- [ ] PluginsSettingsPage 无额外边框包裹
+- [ ] AboutSettingsPage 无额外边框包裹
+
+## Phase 4: 视觉规范
+
+- [ ] 设置项间距统一
+- [ ] 圆角样式统一
+- [ ] 页面标题样式统一
+
+## 验证
+
+- [ ] 编译通过,无错误
+- [ ] 运行正常,设置页面可正常显示
+- [ ] 视觉效果符合 Fluent 设计风格
diff --git a/.trae/specs/settings-page-fluent-redesign/spec.md b/.trae/specs/settings-page-fluent-redesign/spec.md
new file mode 100644
index 0000000..ee4fd58
--- /dev/null
+++ b/.trae/specs/settings-page-fluent-redesign/spec.md
@@ -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 包裹改为直接内容布局
diff --git a/.trae/specs/settings-page-fluent-redesign/tasks.md b/.trae/specs/settings-page-fluent-redesign/tasks.md
new file mode 100644
index 0000000..b5a962d
--- /dev/null
+++ b/.trae/specs/settings-page-fluent-redesign/tasks.md
@@ -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
diff --git a/LanMountainDesktop.PluginSdk/ISettingsPageHostContext.cs b/LanMountainDesktop.PluginSdk/ISettingsPageHostContext.cs
new file mode 100644
index 0000000..fc4ad36
--- /dev/null
+++ b/LanMountainDesktop.PluginSdk/ISettingsPageHostContext.cs
@@ -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);
+}
diff --git a/LanMountainDesktop.PluginSdk/PluginManifest.cs b/LanMountainDesktop.PluginSdk/PluginManifest.cs
index 04a0fc8..ffa4717 100644
--- a/LanMountainDesktop.PluginSdk/PluginManifest.cs
+++ b/LanMountainDesktop.PluginSdk/PluginManifest.cs
@@ -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;
diff --git a/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs b/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs
index 0ff6581..b65e669 100644
--- a/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs
+++ b/LanMountainDesktop.PluginSdk/PluginSdkInfo.cs
@@ -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";
diff --git a/LanMountainDesktop.PluginSdk/SettingsPageBase.cs b/LanMountainDesktop.PluginSdk/SettingsPageBase.cs
new file mode 100644
index 0000000..d70e110
--- /dev/null
+++ b/LanMountainDesktop.PluginSdk/SettingsPageBase.cs
@@ -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);
+ }
+}
diff --git a/LanMountainDesktop.PluginSdk/SettingsPageCategory.cs b/LanMountainDesktop.PluginSdk/SettingsPageCategory.cs
new file mode 100644
index 0000000..5b52baa
--- /dev/null
+++ b/LanMountainDesktop.PluginSdk/SettingsPageCategory.cs
@@ -0,0 +1,10 @@
+namespace LanMountainDesktop.PluginSdk;
+
+public enum SettingsPageCategory
+{
+ General = 0,
+ Appearance = 10,
+ Components = 20,
+ Plugins = 30,
+ About = 40
+}
diff --git a/LanMountainDesktop.PluginSdk/SettingsPageInfoAttribute.cs b/LanMountainDesktop.PluginSdk/SettingsPageInfoAttribute.cs
new file mode 100644
index 0000000..950132d
--- /dev/null
+++ b/LanMountainDesktop.PluginSdk/SettingsPageInfoAttribute.cs
@@ -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;
+}
diff --git a/LanMountainDesktop/App.axaml b/LanMountainDesktop/App.axaml
index 1eb2491..f4e0fa1 100644
--- a/LanMountainDesktop/App.axaml
+++ b/LanMountainDesktop/App.axaml
@@ -1,4 +1,4 @@
-
+
+
+
+
+
+
+
-
-
-
-
+
+
-
-
+
+
-
-
diff --git a/LanMountainDesktop/Styles/SettingsAnimations.axaml b/LanMountainDesktop/Styles/SettingsAnimations.axaml
index f69a8b2..199fe80 100644
--- a/LanMountainDesktop/Styles/SettingsAnimations.axaml
+++ b/LanMountainDesktop/Styles/SettingsAnimations.axaml
@@ -71,7 +71,7 @@
-
-
-
-
diff --git a/LanMountainDesktop/Styles/SettingsCardStyles.axaml b/LanMountainDesktop/Styles/SettingsCardStyles.axaml
new file mode 100644
index 0000000..60d8aa9
--- /dev/null
+++ b/LanMountainDesktop/Styles/SettingsCardStyles.axaml
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/ViewModels/ComponentLibraryWindowViewModel.cs b/LanMountainDesktop/ViewModels/ComponentLibraryWindowViewModel.cs
new file mode 100644
index 0000000..f68d294
--- /dev/null
+++ b/LanMountainDesktop/ViewModels/ComponentLibraryWindowViewModel.cs
@@ -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 Categories { get; } = [];
+
+ public ObservableCollection Components { get; } = [];
+}
+
+public sealed class ComponentLibraryCategoryViewModel
+{
+ public ComponentLibraryCategoryViewModel(
+ string id,
+ string title,
+ Symbol icon,
+ IReadOnlyList components)
+ {
+ Id = id;
+ Title = title;
+ Icon = icon;
+ Components = components;
+ }
+
+ public string Id { get; }
+
+ public string Title { get; }
+
+ public Symbol Icon { get; }
+
+ public IReadOnlyList 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; }
+}
diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
new file mode 100644
index 0000000..d1dbe9b
--- /dev/null
+++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
@@ -0,0 +1,1299 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia.Media;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using LanMountainDesktop.ComponentSystem;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Services;
+using LanMountainDesktop.Services.Settings;
+
+namespace LanMountainDesktop.ViewModels;
+
+public sealed partial class SettingsWindowViewModel : ViewModelBase
+{
+ private readonly LocalizationService _localizationService;
+ private string _languageCode;
+ private string _defaultRestartMessage = string.Empty;
+
+ public SettingsWindowViewModel()
+ {
+ _localizationService = new();
+ _languageCode = "zh-CN";
+ }
+
+ public SettingsWindowViewModel(LocalizationService localizationService, string languageCode)
+ {
+ _localizationService = localizationService;
+ _languageCode = languageCode;
+ }
+
+ private string L(string key) => _localizationService.GetString(_languageCode, key, key);
+
+ [ObservableProperty]
+ private string _title = string.Empty;
+
+ [ObservableProperty]
+ private string _currentPageTitle = string.Empty;
+
+ [ObservableProperty]
+ private string? _currentPageDescription;
+
+ [ObservableProperty]
+ private string? _currentPageId;
+
+ [ObservableProperty]
+ private bool _isRestartRequested;
+
+ [ObservableProperty]
+ private string _restartMessage = string.Empty;
+
+ [ObservableProperty]
+ private string _restartTitle = string.Empty;
+
+ [ObservableProperty]
+ private string _restartButtonText = string.Empty;
+
+ [ObservableProperty]
+ private string? _drawerTitle;
+
+ [ObservableProperty]
+ private string _drawerFallbackTitle = string.Empty;
+
+ [ObservableProperty]
+ private bool _isDrawerOpen;
+
+ public SettingsWindowViewModel Initialize()
+ {
+ RefreshLanguage(_languageCode);
+ CurrentPageTitle = Title;
+ return this;
+ }
+
+ public void RefreshLanguage(string? languageCode)
+ {
+ _languageCode = _localizationService.NormalizeLanguageCode(languageCode);
+ Title = L("settings.title");
+ RestartTitle = L("settings.restart_dock.title");
+ RestartButtonText = L("settings.restart_dock.button");
+ DrawerFallbackTitle = L("settings.window.drawer_default");
+
+ var nextDefaultRestartMessage = L("settings.restart_dock.description");
+ if (string.IsNullOrWhiteSpace(RestartMessage) || string.Equals(RestartMessage, _defaultRestartMessage, StringComparison.Ordinal))
+ {
+ RestartMessage = nextDefaultRestartMessage;
+ }
+
+ _defaultRestartMessage = nextDefaultRestartMessage;
+ }
+
+ public string GetDefaultRestartMessage() => _defaultRestartMessage;
+
+ public ObservableCollection Pages { get; } = [];
+}
+
+public sealed class SelectionOption
+{
+ public SelectionOption(string value, string label)
+ {
+ Value = value;
+ Label = label;
+ }
+
+ public string Value { get; }
+
+ public string Label { get; }
+}
+
+public sealed class TimeZoneOption
+{
+ public TimeZoneOption(string? id, string label)
+ {
+ Id = id;
+ Label = label;
+ }
+
+ public string? Id { get; }
+
+ public string Label { get; }
+}
+
+public sealed partial class GeneralSettingsPageViewModel : ViewModelBase
+{
+ private readonly ISettingsFacadeService _settingsFacade;
+ private readonly TimeZoneService _timeZoneService;
+ private readonly LocalizationService _localizationService = new();
+ private readonly string _startupRenderMode;
+ private string _languageCode;
+ private bool _isInitializing;
+
+ public GeneralSettingsPageViewModel(ISettingsFacadeService settingsFacade)
+ {
+ _settingsFacade = settingsFacade;
+ _timeZoneService = settingsFacade.Region.GetTimeZoneService();
+ _startupRenderMode = Program.StartupRenderMode;
+
+ var regionState = _settingsFacade.Region.Get();
+ _languageCode = _localizationService.NormalizeLanguageCode(regionState.LanguageCode);
+
+ Languages = CreateLanguageOptions();
+ RenderModes = CreateRenderModeOptions();
+ TimeZones = CreateTimeZoneOptions();
+ RefreshLocalizedText();
+
+ _isInitializing = true;
+ SelectedLanguage = Languages.FirstOrDefault(option =>
+ string.Equals(option.Value, regionState.LanguageCode, StringComparison.OrdinalIgnoreCase))
+ ?? Languages[0];
+ SelectedTimeZone = TimeZones.FirstOrDefault(option =>
+ string.Equals(option.Id, regionState.TimeZoneId, StringComparison.OrdinalIgnoreCase))
+ ?? TimeZones[0];
+
+ var appSnapshot = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App);
+ var normalizedRenderMode = AppRenderingModeHelper.Normalize(appSnapshot.AppRenderMode);
+ SelectedRenderMode = RenderModes.FirstOrDefault(option =>
+ string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase))
+ ?? RenderModes[0];
+ _isInitializing = false;
+
+ RefreshPreview();
+ }
+
+ public event Action? RestartRequested;
+
+ public IReadOnlyList Languages { get; }
+
+ public IReadOnlyList RenderModes { get; }
+
+ public IReadOnlyList TimeZones { get; }
+
+ [ObservableProperty]
+ private SelectionOption _selectedLanguage = new("zh-CN", "中文");
+
+ [ObservableProperty]
+ private TimeZoneOption _selectedTimeZone = new(null, "Follow system default");
+
+ [ObservableProperty]
+ private SelectionOption _selectedRenderMode = new(AppRenderingModeHelper.Default, "Default");
+
+ [ObservableProperty]
+ private string _pageTitle = string.Empty;
+
+ [ObservableProperty]
+ private string _pageDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _basicHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _runtimeHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _runtimeDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _languageHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _timeZoneHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _timeZoneDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _renderModeHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _previewHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _previewTimeLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _previewDateLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _previewTimeText = string.Empty;
+
+ [ObservableProperty]
+ private string _previewDateText = string.Empty;
+
+ [ObservableProperty]
+ private string _renderModeRestartMessage = string.Empty;
+
+ partial void OnSelectedLanguageChanged(SelectionOption value)
+ {
+ RefreshPreview();
+ if (_isInitializing || value is null)
+ {
+ return;
+ }
+
+ _settingsFacade.Region.Save(new RegionSettingsState(
+ value.Value,
+ NormalizeTimeZoneId(SelectedTimeZone?.Id)));
+ }
+
+ partial void OnSelectedTimeZoneChanged(TimeZoneOption value)
+ {
+ RefreshPreview();
+ if (_isInitializing || value is null)
+ {
+ return;
+ }
+
+ _settingsFacade.Region.Save(new RegionSettingsState(
+ SelectedLanguage.Value,
+ NormalizeTimeZoneId(value.Id)));
+ }
+
+ partial void OnSelectedRenderModeChanged(SelectionOption value)
+ {
+ if (_isInitializing || value is null)
+ {
+ return;
+ }
+
+ var appSnapshot = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App);
+ var normalizedRenderMode = AppRenderingModeHelper.Normalize(value.Value);
+ appSnapshot.AppRenderMode = normalizedRenderMode;
+ _settingsFacade.Settings.SaveSnapshot(
+ SettingsScope.App,
+ appSnapshot,
+ changedKeys: [nameof(AppSettingsSnapshot.AppRenderMode)]);
+
+ var restartRequired = !string.Equals(_startupRenderMode, normalizedRenderMode, StringComparison.OrdinalIgnoreCase);
+ PendingRestartStateService.SetPending(PendingRestartStateService.RenderModeReason, restartRequired);
+ if (restartRequired)
+ {
+ RestartRequested?.Invoke();
+ }
+ }
+
+ private IReadOnlyList CreateLanguageOptions()
+ {
+ return
+ [
+ new SelectionOption("zh-CN", L("settings.region.language_zh", "中文")),
+ new SelectionOption("en-US", L("settings.region.language_en", "English"))
+ ];
+ }
+
+ private IReadOnlyList CreateRenderModeOptions()
+ {
+ return
+ [
+ new SelectionOption(AppRenderingModeHelper.Default, L("settings.about.render_mode.default", "Default")),
+ new SelectionOption(AppRenderingModeHelper.Software, L("settings.about.render_mode.software", "Software")),
+ new SelectionOption(AppRenderingModeHelper.AngleEgl, L("settings.about.render_mode.angle_egl", "Angle EGL")),
+ new SelectionOption(AppRenderingModeHelper.Wgl, L("settings.about.render_mode.wgl", "WGL")),
+ new SelectionOption(AppRenderingModeHelper.Vulkan, L("settings.about.render_mode.vulkan", "Vulkan"))
+ ];
+ }
+
+ private IReadOnlyList CreateTimeZoneOptions()
+ {
+ return _timeZoneService
+ .GetAllTimeZones()
+ .Select(zone => new TimeZoneOption(zone.Id, _timeZoneService.GetTimeZoneDisplayName(zone)))
+ .Prepend(new TimeZoneOption(null, L("settings.region.follow_system", "Follow system default")))
+ .ToList();
+ }
+
+ private void RefreshLocalizedText()
+ {
+ PageTitle = L("settings.general.title", "General");
+ PageDescription = L("settings.general.description", "Core language, time zone, and runtime behavior.");
+ BasicHeader = L("settings.general.basic_header", "Basic Settings");
+ RuntimeHeader = L("settings.general.runtime_header", "Runtime");
+ RuntimeDescription = L(
+ "settings.about.render_mode_desc",
+ "Choose the rendering backend. Restart the app after changing this option.");
+ LanguageHeader = L("settings.region.language_header", "Language");
+ TimeZoneHeader = L("settings.region.timezone_header", "Time Zone");
+ TimeZoneDescription = L(
+ "settings.region.timezone_desc",
+ "Select a time zone. Clock and calendar widgets will follow this zone.");
+ RenderModeHeader = L("settings.about.render_mode_header", "App Rendering Mode");
+ PreviewHeader = L("settings.general.preview_header", "Date & Time Preview");
+ PreviewTimeLabel = L("settings.general.preview_time_label", "Time");
+ PreviewDateLabel = L("settings.general.preview_date_label", "Date");
+ RenderModeRestartMessage = L(
+ "settings.general.render_mode_restart_message",
+ "Rendering mode changes require restarting the app.");
+ }
+
+ private void RefreshPreview()
+ {
+ var culture = ResolveCulture(SelectedLanguage?.Value ?? _languageCode);
+ var timeZone = ResolveSelectedTimeZone();
+ var now = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, timeZone);
+ PreviewTimeText = now.ToString("T", culture);
+ PreviewDateText = now.ToString("D", culture);
+ }
+
+ private TimeZoneInfo ResolveSelectedTimeZone()
+ {
+ var timeZoneId = NormalizeTimeZoneId(SelectedTimeZone?.Id);
+ if (string.IsNullOrWhiteSpace(timeZoneId))
+ {
+ return TimeZoneInfo.Local;
+ }
+
+ try
+ {
+ return TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
+ }
+ catch (TimeZoneNotFoundException)
+ {
+ return TimeZoneInfo.Local;
+ }
+ catch (InvalidTimeZoneException)
+ {
+ return TimeZoneInfo.Local;
+ }
+ }
+
+ private string? NormalizeTimeZoneId(string? value)
+ => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
+
+ private CultureInfo ResolveCulture(string? languageCode)
+ {
+ var normalizedLanguageCode = _localizationService.NormalizeLanguageCode(languageCode);
+ try
+ {
+ return CultureInfo.GetCultureInfo(normalizedLanguageCode);
+ }
+ catch (CultureNotFoundException)
+ {
+ return CultureInfo.GetCultureInfo("zh-CN");
+ }
+ }
+
+ private string L(string key, string fallback)
+ => _localizationService.GetString(_languageCode, key, fallback);
+}
+
+public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
+{
+ private readonly ISettingsFacadeService _settingsFacade;
+ private readonly LocalizationService _localizationService = new();
+ private readonly string _languageCode;
+ private bool _isInitializing;
+
+ public AppearanceSettingsPageViewModel(ISettingsFacadeService settingsFacade)
+ {
+ _settingsFacade = settingsFacade;
+ _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
+ WallpaperPlacements = CreateWallpaperPlacements();
+ ClockFormats = CreateClockFormats();
+ RefreshLocalizedText();
+
+ _isInitializing = true;
+ Load();
+ _isInitializing = false;
+ }
+
+ public IReadOnlyList WallpaperPlacements { get; }
+
+ public IReadOnlyList ClockFormats { get; }
+
+ [ObservableProperty]
+ private bool _isNightMode;
+
+ [ObservableProperty]
+ private string _themeColor = string.Empty;
+
+ [ObservableProperty]
+ private bool _useSystemChrome;
+
+ [ObservableProperty]
+ private string _wallpaperPath = string.Empty;
+
+ [ObservableProperty]
+ private SelectionOption _selectedWallpaperPlacement = new("Fill", "Fill");
+
+ [ObservableProperty]
+ private bool _showClock = true;
+
+ [ObservableProperty]
+ private SelectionOption _selectedClockFormat = new("HourMinuteSecond", "Hour:Minute:Second");
+
+ [ObservableProperty]
+ private string _pageTitle = string.Empty;
+
+ [ObservableProperty]
+ private string _pageDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _nightModeLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _useSystemChromeLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _themeColorLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _themeHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _wallpaperHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _wallpaperPathLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _wallpaperPlacementLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _importWallpaperButtonText = string.Empty;
+
+ [ObservableProperty]
+ private string _clockHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _clockDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _clockFormatLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _filePickerTitle = string.Empty;
+
+ public void Load()
+ {
+ var theme = _settingsFacade.Theme.Get();
+ IsNightMode = theme.IsNightMode;
+ ThemeColor = theme.ThemeColor ?? string.Empty;
+ UseSystemChrome = theme.UseSystemChrome;
+
+ var wallpaper = _settingsFacade.Wallpaper.Get();
+ WallpaperPath = wallpaper.WallpaperPath ?? string.Empty;
+ var wallpaperPlacement = string.IsNullOrWhiteSpace(wallpaper.Placement)
+ ? "Fill"
+ : wallpaper.Placement;
+ SelectedWallpaperPlacement = WallpaperPlacements.FirstOrDefault(option =>
+ string.Equals(option.Value, wallpaperPlacement, StringComparison.OrdinalIgnoreCase))
+ ?? WallpaperPlacements[0];
+
+ var statusBar = _settingsFacade.StatusBar.Get();
+ ShowClock = statusBar.TopStatusComponentIds.Any(id =>
+ string.Equals(id, BuiltInComponentIds.Clock, StringComparison.OrdinalIgnoreCase));
+ var clockFormat = string.IsNullOrWhiteSpace(statusBar.ClockDisplayFormat)
+ ? "HourMinuteSecond"
+ : statusBar.ClockDisplayFormat;
+ SelectedClockFormat = ClockFormats.FirstOrDefault(option =>
+ string.Equals(option.Value, clockFormat, StringComparison.OrdinalIgnoreCase))
+ ?? ClockFormats[1];
+ }
+
+ public async Task ImportWallpaperAsync(string sourcePath)
+ {
+ var importedPath = await _settingsFacade.WallpaperMedia.ImportAssetAsync(sourcePath);
+ if (!string.IsNullOrWhiteSpace(importedPath))
+ {
+ WallpaperPath = importedPath;
+ }
+ }
+
+ partial void OnIsNightModeChanged(bool value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ SaveTheme();
+ }
+
+ partial void OnThemeColorChanged(string value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(value) || Color.TryParse(value, out _))
+ {
+ SaveTheme();
+ }
+ }
+
+ partial void OnUseSystemChromeChanged(bool value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ SaveTheme();
+ }
+
+ partial void OnWallpaperPathChanged(string value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ SaveWallpaper();
+ }
+
+ partial void OnSelectedWallpaperPlacementChanged(SelectionOption value)
+ {
+ if (_isInitializing || value is null)
+ {
+ return;
+ }
+
+ SaveWallpaper();
+ }
+
+ partial void OnShowClockChanged(bool value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ SaveStatusBar();
+ }
+
+ partial void OnSelectedClockFormatChanged(SelectionOption value)
+ {
+ if (_isInitializing || value is null)
+ {
+ return;
+ }
+
+ SaveStatusBar();
+ }
+
+ private void SaveTheme()
+ {
+ _settingsFacade.Theme.Save(new ThemeAppearanceSettingsState(
+ IsNightMode,
+ string.IsNullOrWhiteSpace(ThemeColor) ? null : ThemeColor,
+ UseSystemChrome));
+ }
+
+ private void SaveWallpaper()
+ {
+ _settingsFacade.Wallpaper.Save(new WallpaperSettingsState(
+ string.IsNullOrWhiteSpace(WallpaperPath) ? null : WallpaperPath,
+ SelectedWallpaperPlacement.Value));
+ }
+
+ private void SaveStatusBar()
+ {
+ var state = _settingsFacade.StatusBar.Get();
+ var topComponents = state.TopStatusComponentIds
+ .Where(id => !string.Equals(id, BuiltInComponentIds.Clock, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ if (ShowClock)
+ {
+ topComponents.Add(BuiltInComponentIds.Clock);
+ }
+
+ _settingsFacade.StatusBar.Save(new StatusBarSettingsState(
+ topComponents,
+ state.PinnedTaskbarActions,
+ state.EnableDynamicTaskbarActions,
+ state.TaskbarLayoutMode,
+ SelectedClockFormat.Value,
+ state.SpacingMode,
+ state.CustomSpacingPercent));
+ }
+
+ private IReadOnlyList CreateWallpaperPlacements()
+ {
+ return
+ [
+ new SelectionOption("Fill", L("settings.wallpaper.placement.fill", "Fill")),
+ new SelectionOption("Fit", L("settings.wallpaper.placement.fit", "Fit")),
+ new SelectionOption("Stretch", L("settings.wallpaper.placement.stretch", "Stretch")),
+ new SelectionOption("Center", L("settings.wallpaper.placement.center", "Center")),
+ new SelectionOption("Tile", L("settings.wallpaper.placement.tile", "Tile"))
+ ];
+ }
+
+ private IReadOnlyList CreateClockFormats()
+ {
+ return
+ [
+ new SelectionOption("HourMinute", L("settings.status_bar.clock_format.hm", "Hour:Minute")),
+ new SelectionOption("HourMinuteSecond", L("settings.status_bar.clock_format.hms", "Hour:Minute:Second"))
+ ];
+ }
+
+ private void RefreshLocalizedText()
+ {
+ PageTitle = L("settings.appearance.title", "Appearance");
+ PageDescription = L("settings.appearance.description", "Theme, wallpaper, and status bar presentation.");
+ ThemeHeader = L("settings.appearance.theme_header", "Theme");
+ NightModeLabel = L("settings.color.enable_night_mode_toggle", "Enable night mode");
+ UseSystemChromeLabel = L("settings.color.use_system_chrome_toggle", "Use system window chrome");
+ ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color");
+ WallpaperHeader = L("settings.wallpaper.title", "Wallpaper");
+ WallpaperPathLabel = L("settings.wallpaper.current_label", "Current Wallpaper");
+ WallpaperPlacementLabel = L("settings.wallpaper.placement_label", "Placement");
+ ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper");
+ ClockHeader = L("settings.status_bar.clock_header", "Clock Component");
+ ClockDescription = L("settings.status_bar.clock_description", "Display a clock on the top status bar.");
+ ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock Format");
+ FilePickerTitle = L("filepicker.title", "Select wallpaper");
+ }
+
+ private string L(string key, string fallback)
+ => _localizationService.GetString(_languageCode, key, fallback);
+}
+
+public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
+{
+ private readonly ISettingsFacadeService _settingsFacade;
+ private readonly LocalizationService _localizationService = new();
+ private readonly string _languageCode;
+ private bool _isInitializing;
+
+ public ComponentsSettingsPageViewModel(ISettingsFacadeService settingsFacade)
+ {
+ _settingsFacade = settingsFacade;
+ _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
+ SpacingPresets = CreateSpacingPresets();
+ RefreshLocalizedText();
+
+ _isInitializing = true;
+ Load();
+ _isInitializing = false;
+ }
+
+ public IReadOnlyList SpacingPresets { get; }
+
+ [ObservableProperty]
+ private int _shortSideCells;
+
+ [ObservableProperty]
+ private int _edgeInsetPercent;
+
+ [ObservableProperty]
+ private SelectionOption _selectedSpacingPreset = new("Relaxed", "Relaxed");
+
+ [ObservableProperty]
+ private string _pageTitle = string.Empty;
+
+ [ObservableProperty]
+ private string _pageDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _gridHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _shortSideCellsLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _edgeInsetPercentLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _spacingPresetLabel = string.Empty;
+
+ public void Load()
+ {
+ var state = _settingsFacade.Grid.Get();
+ ShortSideCells = state.ShortSideCells;
+ EdgeInsetPercent = state.EdgeInsetPercent;
+ var spacingPreset = _settingsFacade.Grid.NormalizeSpacingPreset(state.SpacingPreset);
+ SelectedSpacingPreset = SpacingPresets.FirstOrDefault(option =>
+ string.Equals(option.Value, spacingPreset, StringComparison.OrdinalIgnoreCase))
+ ?? SpacingPresets[1];
+ }
+
+ partial void OnShortSideCellsChanged(int value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ SaveGrid();
+ }
+
+ partial void OnEdgeInsetPercentChanged(int value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ SaveGrid();
+ }
+
+ partial void OnSelectedSpacingPresetChanged(SelectionOption value)
+ {
+ if (_isInitializing || value is null)
+ {
+ return;
+ }
+
+ SaveGrid();
+ }
+
+ private void SaveGrid()
+ {
+ _settingsFacade.Grid.Save(new GridSettingsState(
+ Math.Clamp(ShortSideCells, 6, 96),
+ _settingsFacade.Grid.NormalizeSpacingPreset(SelectedSpacingPreset.Value),
+ Math.Clamp(EdgeInsetPercent, 0, 30)));
+ }
+
+ private IReadOnlyList CreateSpacingPresets()
+ {
+ return
+ [
+ new SelectionOption("Compact", L("settings.grid.spacing_compact", "Compact")),
+ new SelectionOption("Relaxed", L("settings.grid.spacing_relaxed", "Relaxed"))
+ ];
+ }
+
+ private void RefreshLocalizedText()
+ {
+ PageTitle = L("settings.components.title", "Components");
+ PageDescription = L("settings.components.description", "Desktop grid and widget placement density.");
+ GridHeader = L("settings.components.grid_header", "Grid Layout");
+ ShortSideCellsLabel = L("settings.grid.short_side_label", "Short Side Cells");
+ EdgeInsetPercentLabel = L("settings.grid.edge_inset_label", "Screen Inset");
+ SpacingPresetLabel = L("settings.grid.spacing_label", "Grid Spacing");
+ }
+
+ private string L(string key, string fallback)
+ => _localizationService.GetString(_languageCode, key, fallback);
+}
+
+public sealed partial class InstalledPluginItemViewModel : ViewModelBase
+{
+ public InstalledPluginItemViewModel(InstalledPluginInfo info)
+ {
+ PluginId = info.Manifest.Id;
+ Name = info.Manifest.Name;
+ Version = info.Manifest.Version ?? "-";
+ Description = info.Manifest.Description;
+ ErrorMessage = info.ErrorMessage;
+ IsLoaded = info.IsLoaded;
+ IsPackage = info.IsPackage;
+ IsEnabled = info.IsEnabled;
+ }
+
+ public string PluginId { get; }
+
+ public string Name { get; }
+
+ public string Version { get; }
+
+ public string? Description { get; }
+
+ public string? ErrorMessage { get; }
+
+ public bool IsLoaded { get; }
+
+ public bool IsPackage { get; }
+
+ [ObservableProperty]
+ private bool _isEnabled;
+}
+
+public sealed class PluginMarketItemViewModel
+{
+ public PluginMarketItemViewModel(PluginMarketPluginInfo plugin)
+ {
+ PluginId = plugin.Id;
+ Name = plugin.Name;
+ Description = plugin.Description;
+ Version = plugin.Version;
+ Author = plugin.Author;
+ ApiVersion = plugin.ApiVersion;
+ }
+
+ public string PluginId { get; }
+
+ public string Name { get; }
+
+ public string Description { get; }
+
+ public string Version { get; }
+
+ public string Author { get; }
+
+ public string ApiVersion { get; }
+}
+
+public sealed partial class PluginsSettingsPageViewModel : ViewModelBase
+{
+ private readonly ISettingsFacadeService _settingsFacade;
+ private readonly LocalizationService _localizationService = new();
+ private readonly string _languageCode;
+
+ public PluginsSettingsPageViewModel(ISettingsFacadeService settingsFacade)
+ {
+ _settingsFacade = settingsFacade;
+ _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
+ RefreshLocalizedText();
+ StatusMessage = L(
+ "settings.plugins.initial_status",
+ "Refresh plugin state to see the latest installed and marketplace entries.");
+ }
+
+ public event Action? RestartRequested;
+
+ public ObservableCollection InstalledPlugins { get; } = [];
+
+ public ObservableCollection MarketPlugins { get; } = [];
+
+ [ObservableProperty]
+ private string _statusMessage = string.Empty;
+
+ [ObservableProperty]
+ private bool _isBusy;
+
+ [ObservableProperty]
+ private string _pageTitle = string.Empty;
+
+ [ObservableProperty]
+ private string _pageDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _refreshButtonText = string.Empty;
+
+ [ObservableProperty]
+ private string _installedHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _marketplaceHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _deleteButtonText = string.Empty;
+
+ [ObservableProperty]
+ private string _installButtonText = string.Empty;
+
+ [ObservableProperty]
+ private string _emptyInstalledText = string.Empty;
+
+ [ObservableProperty]
+ private string _emptyMarketplaceText = string.Empty;
+
+ [ObservableProperty]
+ private string _restartRequiredMessage = string.Empty;
+
+ public async Task InitializeAsync()
+ {
+ if (InstalledPlugins.Count > 0 || MarketPlugins.Count > 0)
+ {
+ return;
+ }
+
+ await RefreshAsync();
+ }
+
+ [RelayCommand]
+ private async Task RefreshAsync()
+ {
+ if (IsBusy)
+ {
+ return;
+ }
+
+ try
+ {
+ IsBusy = true;
+
+ InstalledPlugins.Clear();
+ foreach (var plugin in _settingsFacade.PluginManagement.GetInstalledPlugins())
+ {
+ InstalledPlugins.Add(new InstalledPluginItemViewModel(plugin));
+ }
+
+ MarketPlugins.Clear();
+ var marketResult = await _settingsFacade.PluginMarket.LoadIndexAsync();
+ if (marketResult.Success)
+ {
+ foreach (var plugin in marketResult.Plugins)
+ {
+ MarketPlugins.Add(new PluginMarketItemViewModel(plugin));
+ }
+
+ StatusMessage = string.Format(
+ CultureInfo.CurrentCulture,
+ L(
+ "settings.plugins.refresh_success_format",
+ "Loaded {0} installed plugins and {1} marketplace entries."),
+ InstalledPlugins.Count,
+ MarketPlugins.Count);
+ }
+ else
+ {
+ StatusMessage = string.IsNullOrWhiteSpace(marketResult.ErrorMessage)
+ ? L("settings.plugins.refresh_failed", "Failed to load plugin market index.")
+ : marketResult.ErrorMessage;
+ }
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ [RelayCommand]
+ private void TogglePlugin(InstalledPluginItemViewModel? item)
+ {
+ if (item is null)
+ {
+ return;
+ }
+
+ if (_settingsFacade.PluginManagement.SetPluginEnabled(item.PluginId, item.IsEnabled))
+ {
+ StatusMessage = string.Format(
+ CultureInfo.CurrentCulture,
+ L(
+ "settings.plugins.toggle_result_format",
+ "Plugin '{0}' was {1} for the next launch. Restart the app to apply page and widget changes."),
+ item.Name,
+ item.IsEnabled
+ ? L("settings.plugins.toggle_state_enabled", "enabled")
+ : L("settings.plugins.toggle_state_disabled", "disabled"));
+ RestartRequested?.Invoke();
+ }
+ else
+ {
+ item.IsEnabled = !item.IsEnabled;
+ StatusMessage = string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.plugins.toggle_unchanged_format", "Plugin '{0}' did not change."),
+ item.Name);
+ }
+ }
+
+ [RelayCommand]
+ private void DeletePlugin(InstalledPluginItemViewModel? item)
+ {
+ if (item is null)
+ {
+ return;
+ }
+
+ if (_settingsFacade.PluginManagement.DeleteInstalledPlugin(item.PluginId))
+ {
+ InstalledPlugins.Remove(item);
+ StatusMessage = string.Format(
+ CultureInfo.CurrentCulture,
+ L(
+ "settings.plugins.delete_success_format",
+ "Plugin '{0}' was staged for deletion. Restart the app to finish removing it."),
+ item.Name);
+ RestartRequested?.Invoke();
+ }
+ else
+ {
+ StatusMessage = string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.plugins.delete_failed_name_format", "Failed to remove plugin '{0}'."),
+ item.Name);
+ }
+ }
+
+ [RelayCommand]
+ private async Task InstallPluginAsync(PluginMarketItemViewModel? item)
+ {
+ if (item is null || IsBusy)
+ {
+ return;
+ }
+
+ try
+ {
+ IsBusy = true;
+ var result = await _settingsFacade.PluginMarket.InstallAsync(item.PluginId);
+ if (result.Success)
+ {
+ StatusMessage = string.Format(
+ CultureInfo.CurrentCulture,
+ L(
+ "settings.plugins.install_success_format",
+ "Installed plugin '{0}'. Restart the app to apply newly added settings pages and widgets."),
+ item.Name);
+ RestartRequested?.Invoke();
+ await RefreshAsync();
+ return;
+ }
+
+ StatusMessage = string.IsNullOrWhiteSpace(result.ErrorMessage)
+ ? string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.plugins.install_failed_name_format", "Failed to install '{0}'."),
+ item.Name)
+ : result.ErrorMessage;
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private void RefreshLocalizedText()
+ {
+ PageTitle = L("settings.plugins.title", "Plugins");
+ PageDescription = L("settings.plugins.description", "Manage installed plugins and discover marketplace packages.");
+ RefreshButtonText = L("settings.plugins.refresh_button", "Refresh Plugins");
+ InstalledHeader = L("settings.plugins.installed_header", "Installed Plugins");
+ MarketplaceHeader = L("settings.plugins.marketplace_header", "Marketplace");
+ DeleteButtonText = L("settings.plugins.delete_button_short", "Delete");
+ InstallButtonText = L("settings.plugins.install_button_short", "Install");
+ EmptyInstalledText = L("settings.plugins.empty", "No plugins found.");
+ EmptyMarketplaceText = L("settings.plugins.marketplace_empty", "No marketplace plugins available.");
+ RestartRequiredMessage = L("settings.plugins.restart_required", "Plugin changes take effect after restart.");
+ }
+
+ private string L(string key, string fallback)
+ => _localizationService.GetString(_languageCode, key, fallback);
+}
+
+public sealed partial class AboutSettingsPageViewModel : ViewModelBase
+{
+ private readonly ISettingsFacadeService _settingsFacade;
+ private readonly LocalizationService _localizationService = new();
+ private readonly string _languageCode;
+ private bool _isInitializing;
+
+ public AboutSettingsPageViewModel(ISettingsFacadeService settingsFacade)
+ {
+ _settingsFacade = settingsFacade;
+ _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
+ UpdateChannels = CreateUpdateChannels();
+ RefreshLocalizedText();
+
+ var update = _settingsFacade.Update.Get();
+ _isInitializing = true;
+ AutoCheckUpdates = update.AutoCheckUpdates;
+ IncludePrereleaseUpdates = update.IncludePrereleaseUpdates;
+ SelectedUpdateChannel = UpdateChannels.FirstOrDefault(option =>
+ string.Equals(
+ option.Value,
+ string.IsNullOrWhiteSpace(update.UpdateChannel) ? "stable" : update.UpdateChannel,
+ StringComparison.OrdinalIgnoreCase))
+ ?? UpdateChannels[0];
+
+ var versionText = _settingsFacade.ApplicationInfo.GetAppVersionText();
+ var backendInfo = _settingsFacade.ApplicationInfo.GetRenderBackendInfo();
+ var renderBackendText = string.IsNullOrWhiteSpace(backendInfo.ImplementationTypeName)
+ ? backendInfo.ActualBackend
+ : $"{backendInfo.ActualBackend} ({backendInfo.ImplementationTypeName})";
+ VersionText = string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.about.version_format", "Version: {0}"),
+ versionText);
+ RenderBackendText = string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.about.render_backend_format", "Render Backend: {0}"),
+ renderBackendText);
+ UpdateStatus = L("settings.update.status_idle", "No update check has been performed yet.");
+ _isInitializing = false;
+ }
+
+ public IReadOnlyList UpdateChannels { get; }
+
+ [ObservableProperty]
+ private string _versionText = "-";
+
+ [ObservableProperty]
+ private string _renderBackendText = "-";
+
+ [ObservableProperty]
+ private bool _autoCheckUpdates;
+
+ [ObservableProperty]
+ private bool _includePrereleaseUpdates;
+
+ [ObservableProperty]
+ private SelectionOption _selectedUpdateChannel = new("stable", "Stable");
+
+ [ObservableProperty]
+ private string _updateStatus = string.Empty;
+
+ [ObservableProperty]
+ private bool _isCheckingForUpdates;
+
+ [ObservableProperty]
+ private string _pageTitle = string.Empty;
+
+ [ObservableProperty]
+ private string _pageDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _autoCheckUpdatesLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _includePrereleaseUpdatesLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _updateChannelLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _checkForUpdatesButtonText = string.Empty;
+
+ [ObservableProperty]
+ private string _appInfoHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _updateHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _versionLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _renderBackendLabel = string.Empty;
+
+ partial void OnAutoCheckUpdatesChanged(bool value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ SaveUpdateSettings();
+ }
+
+ partial void OnIncludePrereleaseUpdatesChanged(bool value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ SaveUpdateSettings();
+ }
+
+ partial void OnSelectedUpdateChannelChanged(SelectionOption value)
+ {
+ if (_isInitializing || value is null)
+ {
+ return;
+ }
+
+ SaveUpdateSettings();
+ }
+
+ private void SaveUpdateSettings()
+ {
+ _settingsFacade.Update.Save(new UpdateSettingsState(
+ AutoCheckUpdates,
+ IncludePrereleaseUpdates,
+ SelectedUpdateChannel.Value));
+ UpdateStatus = L("settings.update.status_preferences_saved", "Update preferences saved.");
+ }
+
+ [RelayCommand]
+ private async Task CheckForUpdatesAsync()
+ {
+ if (IsCheckingForUpdates)
+ {
+ return;
+ }
+
+ try
+ {
+ IsCheckingForUpdates = true;
+ var version = Version.TryParse(VersionText, out var currentVersion)
+ ? currentVersion
+ : new Version(0, 0, 0);
+
+ var result = await _settingsFacade.Update.CheckForUpdatesAsync(version, IncludePrereleaseUpdates);
+ if (!result.Success)
+ {
+ UpdateStatus = string.IsNullOrWhiteSpace(result.ErrorMessage)
+ ? L("settings.update.status_check_failed", "Failed to check for updates.")
+ : result.ErrorMessage;
+ return;
+ }
+
+ UpdateStatus = result.IsUpdateAvailable
+ ? string.Format(
+ CultureInfo.CurrentCulture,
+ L(
+ "settings.update.status_available_summary_format",
+ "Update available: {0} (current: {1})"),
+ result.LatestVersionText,
+ result.CurrentVersionText)
+ : string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.update.status_up_to_date_format", "You are up to date ({0})."),
+ result.CurrentVersionText);
+ }
+ finally
+ {
+ IsCheckingForUpdates = false;
+ }
+ }
+
+ private IReadOnlyList CreateUpdateChannels()
+ {
+ return
+ [
+ new SelectionOption("stable", L("settings.update.channel_stable", "Stable")),
+ new SelectionOption("preview", L("settings.update.channel_preview", "Preview"))
+ ];
+ }
+
+ private void RefreshLocalizedText()
+ {
+ PageTitle = L("settings.about.title", "About");
+ PageDescription = L("settings.about.description", "Application details and update preferences.");
+ AppInfoHeader = L("settings.about.app_info_header", "Application Information");
+ UpdateHeader = L("settings.about.update_header", "Updates");
+ VersionLabel = L("settings.about.version_label", "Version");
+ RenderBackendLabel = L("settings.about.render_backend_label", "Render Backend");
+ AutoCheckUpdatesLabel = L("settings.update.auto_check_toggle", "Automatically check for updates on startup");
+ IncludePrereleaseUpdatesLabel = L("settings.update.include_prerelease_toggle", "Include prerelease versions");
+ UpdateChannelLabel = L("settings.update.channel_label", "Update Channel");
+ CheckForUpdatesButtonText = L("settings.update.check_button", "Check for Updates");
+ }
+
+ private string L(string key, string fallback)
+ => _localizationService.GetString(_languageCode, key, fallback);
+}
+
+public sealed class PluginGeneratedSettingsPageViewModel
+{
+ public PluginGeneratedSettingsPageViewModel(
+ ISettingsService settingsService,
+ string pluginId,
+ PluginSettingsSectionRegistration section,
+ PluginLocalizer localizer)
+ {
+ SettingsService = settingsService;
+ PluginId = pluginId;
+ Section = section;
+ Localizer = localizer;
+ Title = localizer.GetString(section.TitleLocalizationKey, section.TitleLocalizationKey);
+ Description = string.IsNullOrWhiteSpace(section.DescriptionLocalizationKey)
+ ? null
+ : localizer.GetString(section.DescriptionLocalizationKey, section.DescriptionLocalizationKey);
+ }
+
+ public ISettingsService SettingsService { get; }
+
+ public string PluginId { get; }
+
+ public PluginSettingsSectionRegistration Section { get; }
+
+ public PluginLocalizer Localizer { get; }
+
+ public string Title { get; }
+
+ public string? Description { get; }
+}
diff --git a/LanMountainDesktop/Views/ComponentLibraryWindow.axaml b/LanMountainDesktop/Views/ComponentLibraryWindow.axaml
new file mode 100644
index 0000000..ecc87d2
--- /dev/null
+++ b/LanMountainDesktop/Views/ComponentLibraryWindow.axaml
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/ComponentLibraryWindow.axaml.cs b/LanMountainDesktop/Views/ComponentLibraryWindow.axaml.cs
new file mode 100644
index 0000000..7e9ef83
--- /dev/null
+++ b/LanMountainDesktop/Views/ComponentLibraryWindow.axaml.cs
@@ -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? _createContextFactory;
+ private Func? _localize;
+ private readonly ComponentLibraryWindowViewModel _viewModel = new();
+
+ public ComponentLibraryWindow()
+ {
+ InitializeComponent();
+ DataContext = _viewModel;
+ }
+
+ public ComponentLibraryWindow(
+ IComponentLibraryService componentLibraryService,
+ Func createContextFactory,
+ Func 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? 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;
+ }
+}
diff --git a/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs
index 801fa4d..7d8aa41 100644
--- a/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs b/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs
index 95ab577..eeee46a 100644
--- a/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/BaiduHotSearchWidget.axaml.cs
@@ -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 _activeItems = [];
private readonly List _hotItemVisuals = [];
diff --git a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs
index 746e3be..4efa154 100644
--- a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs
@@ -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 _activeItems = [];
private readonly List _hotItemVisuals = [];
diff --git a/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs b/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs
index 1911f0f..6408ea3 100644
--- a/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/BrowserWidget.axaml.cs
@@ -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)
diff --git a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs
index bb68223..5cb7dbc 100644
--- a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs
@@ -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();
diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
index 7008f03..a1883c5 100644
--- a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
@@ -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 _newsUrls = [];
@@ -875,4 +875,3 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
return MultiWhitespaceRegex.Replace(text.Trim(), " ");
}
}
-
diff --git a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs
index d4d5f3b..41bdea3 100644
--- a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs
index eec7838..0cd5605 100644
--- a/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DailyPoetryWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs b/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs
index 5faf5d1..d98b885 100644
--- a/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs
index e7368bf..39bb6f3 100644
--- a/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
index 0f20ecd..a09853a 100644
--- a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
+++ b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs
@@ -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());
}
diff --git a/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs b/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs
index 689c3b8..50859c3 100644
--- a/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/ExchangeRateCalculatorWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs
index 66e702d..8d5ac48 100644
--- a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs
index ebd6c62..90d297d 100644
--- a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs
@@ -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 _backgroundBrushCache = new();
private readonly Dictionary _particleBrushCache = new();
diff --git a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs
index 8e6f5b5..c83bdd3 100644
--- a/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/IfengNewsWidget.axaml.cs
@@ -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 _activeItems = [];
private readonly List _itemVisuals = [];
diff --git a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs
index 3e9d43e..0c6b27c 100644
--- a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs
@@ -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 _backgroundBrushCache = new();
private readonly Dictionary _particleBrushCache = new();
diff --git a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs
index 91b96a7..0af0980 100644
--- a/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/MusicControlWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs
index b968868..741eae2 100644
--- a/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/RecordingWidget.axaml.cs
@@ -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 _waveBars = [];
private readonly double[] _waveLevels = new double[WaveBarCount];
diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
index 53efc4c..1e49325 100644
--- a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
@@ -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 _activeItems = [];
private readonly List _itemVisuals = [];
diff --git a/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs
index 815544c..00b2eab 100644
--- a/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyDeductionReasonsWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs
index 42388d0..1e8c3fd 100644
--- a/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs
index 3b5b8ea..cbb2e6f 100644
--- a/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyInterruptDensityWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs
index b0cc36d..542aca4 100644
--- a/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyNoiseCurveWidget.axaml.cs
@@ -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)
{
diff --git a/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs
index e413649..07ab7ac 100644
--- a/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyNoiseDistributionWidget.axaml.cs
@@ -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;
}
}
-
-
diff --git a/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs
index 05b8ef9..1171085 100644
--- a/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudyScoreOverviewWidget.axaml.cs
@@ -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;
diff --git a/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs
index 6a6478f..2148c76 100644
--- a/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudySessionControlWidget.axaml.cs
@@ -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;
}
}
diff --git a/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs
index 0991cda..2a3c1ae 100644
--- a/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/StudySessionHistoryWidget.axaml.cs
@@ -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
}
}
}
-
-
diff --git a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs
index 88e6a58..4ec8ddb 100644
--- a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs
index a53e6bf..19ff550 100644
--- a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs
@@ -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 _backgroundBrushCache = new();
private readonly Dictionary _particleBrushCache = new();
diff --git a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs
index ae7b2d3..2b97c63 100644
--- a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs
@@ -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];
diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
index a8c1826..c86d9c0 100644
--- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
+++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
@@ -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 _componentLibraryCategories = Array.Empty();
- private IReadOnlyList _componentLibraryActiveComponents = Array.Empty();
+ private IReadOnlyList _componentLibraryActiveComponents = Array.Empty();
private bool _isComponentLibraryCategoryGestureActive;
private bool _isComponentLibraryComponentGestureActive;
private Point _componentLibraryCategoryGestureStartPoint;
@@ -95,13 +96,51 @@ public partial class MainWindow
string Id,
Symbol Icon,
string Title,
- IReadOnlyList Components);
+ IReadOnlyList 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 GetComponentLibraryCategories()
{
- var descriptors = _componentRuntimeRegistry.GetDesktopComponents();
- if (descriptors.Count == 0)
+ var categories = _componentLibraryService.GetDesktopCategories();
+ if (categories.Count == 0)
{
return Array.Empty();
}
- 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)
diff --git a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
index dad8e1b..b4db42a 100644
--- a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
+++ b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
@@ -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
diff --git a/LanMountainDesktop/Views/MainWindow.axaml b/LanMountainDesktop/Views/MainWindow.axaml
index 4256e37..7817c5b 100644
--- a/LanMountainDesktop/Views/MainWindow.axaml
+++ b/LanMountainDesktop/Views/MainWindow.axaml
@@ -285,9 +285,9 @@
Grid.Column="2"
Background="Transparent"
BorderThickness="0">
-
-
+
+