From 5fdaa2539b449e60875eff81ed234e117b1cc3a7 Mon Sep 17 00:00:00 2001 From: lincube Date: Fri, 13 Mar 2026 22:20:12 +0800 Subject: [PATCH] settings_re4 --- .../checklist.md | 32 + .../settings-page-fluent-redesign/spec.md | 76 + .../settings-page-fluent-redesign/tasks.md | 51 + .../ISettingsPageHostContext.cs | 12 + .../PluginManifest.cs | 5 +- LanMountainDesktop.PluginSdk/PluginSdkInfo.cs | 2 +- .../SettingsPageBase.cs | 54 + .../SettingsPageCategory.cs | 10 + .../SettingsPageInfoAttribute.cs | 46 + LanMountainDesktop/App.axaml | 3 +- LanMountainDesktop/App.axaml.cs | 133 +- .../DesktopComponentRuntimeContext.cs | 6 +- .../IComponentSettingsContextAware.cs | 18 + .../Controls/SettingsOptionCard.axaml | 37 + .../Controls/SettingsOptionCard.axaml.cs | 112 ++ .../Controls/SettingsSectionCard.axaml | 33 + .../Controls/SettingsSectionCard.axaml.cs | 97 ++ LanMountainDesktop/Localization/en-US.json | 54 +- LanMountainDesktop/Localization/zh-CN.json | 54 +- .../Models/AppSettingsSnapshot.cs | 2 + LanMountainDesktop/Program.cs | 3 + .../Services/ComponentLibraryServices.cs | 78 +- .../Services/ComponentSettingsService.cs | 6 + .../DesktopComponentRegistryFactory.cs | 6 +- .../HostComponentSettingsStoreProvider.cs | 14 + .../Services/IComponentLibraryService.cs | 41 + .../Settings/SettingsCatalogService.cs | 5 +- .../Services/Settings/SettingsContracts.cs | 2 +- .../Settings/SettingsDomainServices.cs | 207 ++- .../Services/Settings/SettingsPageRegistry.cs | 334 +++++ .../SettingsServiceAppSnapshotExtensions.cs | 20 + .../Settings/SettingsWindowService.cs | 306 ++++ LanMountainDesktop/Styles/GlassModule.axaml | 44 +- .../Styles/SettingsAnimations.axaml | 8 +- .../Styles/SettingsCardStyles.axaml | 198 +++ .../ComponentLibraryWindowViewModel.cs | 57 + .../ViewModels/SettingsViewModels.cs | 1299 +++++++++++++++++ .../Views/ComponentLibraryWindow.axaml | 129 ++ .../Views/ComponentLibraryWindow.axaml.cs | 237 +++ .../Components/AnalogClockWidget.axaml.cs | 4 +- .../Components/BaiduHotSearchWidget.axaml.cs | 4 +- .../BilibiliHotSearchWidget.axaml.cs | 4 +- .../Views/Components/BrowserWidget.axaml.cs | 2 - .../Components/ClassScheduleWidget.axaml.cs | 3 +- .../Components/CnrDailyNewsWidget.axaml.cs | 5 +- .../Components/DailyArtworkWidget.axaml.cs | 3 +- .../Components/DailyPoetryWidget.axaml.cs | 2 +- .../Components/DailyWord2x2Widget.axaml.cs | 4 +- .../Views/Components/DailyWordWidget.axaml.cs | 4 +- .../DesktopComponentRuntimeRegistry.cs | 85 +- .../ExchangeRateCalculatorWidget.axaml.cs | 2 +- .../Components/ExtendedWeatherWidget.axaml.cs | 3 +- .../Components/HourlyWeatherWidget.axaml.cs | 4 +- .../Views/Components/IfengNewsWidget.axaml.cs | 4 +- .../Components/MultiDayWeatherWidget.axaml.cs | 4 +- .../Components/MusicControlWidget.axaml.cs | 2 +- .../Views/Components/RecordingWidget.axaml.cs | 2 +- .../Components/Stcn24ForumWidget.axaml.cs | 4 +- .../StudyDeductionReasonsWidget.axaml.cs | 10 +- .../StudyEnvironmentWidget.axaml.cs | 13 +- .../StudyInterruptDensityWidget.axaml.cs | 10 +- .../Components/StudyNoiseCurveWidget.axaml.cs | 24 +- .../StudyNoiseDistributionWidget.axaml.cs | 13 +- .../StudyScoreOverviewWidget.axaml.cs | 10 +- .../StudySessionControlWidget.axaml.cs | 11 +- .../StudySessionHistoryWidget.axaml.cs | 16 +- .../Components/WeatherClockWidget.axaml.cs | 4 +- .../Views/Components/WeatherWidget.axaml.cs | 4 +- .../Components/WorldClockWidget.axaml.cs | 4 +- .../Views/MainWindow.ComponentSystem.cs | 284 +++- .../Views/MainWindow.SettingsHardCut.Stubs.cs | 40 + LanMountainDesktop/Views/MainWindow.axaml | 29 +- LanMountainDesktop/Views/MainWindow.axaml.cs | 39 +- .../SettingsPages/AboutSettingsPage.axaml | 80 + .../SettingsPages/AboutSettingsPage.axaml.cs | 30 + .../AppearanceSettingsPage.axaml | 97 ++ .../AppearanceSettingsPage.axaml.cs | 54 + .../ComponentsSettingsPage.axaml | 50 + .../ComponentsSettingsPage.axaml.cs | 30 + .../SettingsPages/GeneralSettingsPage.axaml | 87 ++ .../GeneralSettingsPage.axaml.cs | 36 + .../GeneratedPluginSettingsPage.axaml | 18 + .../GeneratedPluginSettingsPage.axaml.cs | 226 +++ .../SettingsPages/PluginsSettingsPage.axaml | 87 ++ .../PluginsSettingsPage.axaml.cs | 41 + LanMountainDesktop/Views/SettingsWindow.axaml | 165 +++ .../Views/SettingsWindow.axaml.cs | 467 ++++++ LanMountainDesktop/plugins/PluginLoader.cs | 6 + .../plugins/PluginRuntimeService.cs | 9 + 89 files changed, 5778 insertions(+), 192 deletions(-) create mode 100644 .trae/specs/settings-page-fluent-redesign/checklist.md create mode 100644 .trae/specs/settings-page-fluent-redesign/spec.md create mode 100644 .trae/specs/settings-page-fluent-redesign/tasks.md create mode 100644 LanMountainDesktop.PluginSdk/ISettingsPageHostContext.cs create mode 100644 LanMountainDesktop.PluginSdk/SettingsPageBase.cs create mode 100644 LanMountainDesktop.PluginSdk/SettingsPageCategory.cs create mode 100644 LanMountainDesktop.PluginSdk/SettingsPageInfoAttribute.cs create mode 100644 LanMountainDesktop/ComponentSystem/IComponentSettingsContextAware.cs create mode 100644 LanMountainDesktop/Controls/SettingsOptionCard.axaml create mode 100644 LanMountainDesktop/Controls/SettingsOptionCard.axaml.cs create mode 100644 LanMountainDesktop/Controls/SettingsSectionCard.axaml create mode 100644 LanMountainDesktop/Controls/SettingsSectionCard.axaml.cs create mode 100644 LanMountainDesktop/Services/HostComponentSettingsStoreProvider.cs create mode 100644 LanMountainDesktop/Services/IComponentLibraryService.cs create mode 100644 LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs create mode 100644 LanMountainDesktop/Services/Settings/SettingsServiceAppSnapshotExtensions.cs create mode 100644 LanMountainDesktop/Services/Settings/SettingsWindowService.cs create mode 100644 LanMountainDesktop/Styles/SettingsCardStyles.axaml create mode 100644 LanMountainDesktop/ViewModels/ComponentLibraryWindowViewModel.cs create mode 100644 LanMountainDesktop/ViewModels/SettingsViewModels.cs create mode 100644 LanMountainDesktop/Views/ComponentLibraryWindow.axaml create mode 100644 LanMountainDesktop/Views/ComponentLibraryWindow.axaml.cs create mode 100644 LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml create mode 100644 LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml.cs create mode 100644 LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml create mode 100644 LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs create mode 100644 LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml create mode 100644 LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml.cs create mode 100644 LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml create mode 100644 LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml.cs create mode 100644 LanMountainDesktop/Views/SettingsPages/GeneratedPluginSettingsPage.axaml create mode 100644 LanMountainDesktop/Views/SettingsPages/GeneratedPluginSettingsPage.axaml.cs create mode 100644 LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml create mode 100644 LanMountainDesktop/Views/SettingsPages/PluginsSettingsPage.axaml.cs create mode 100644 LanMountainDesktop/Views/SettingsWindow.axaml create mode 100644 LanMountainDesktop/Views/SettingsWindow.axaml.cs 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"> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanMountainDesktop/Views/SettingsWindow.axaml.cs b/LanMountainDesktop/Views/SettingsWindow.axaml.cs new file mode 100644 index 0000000..d904a5c --- /dev/null +++ b/LanMountainDesktop/Views/SettingsWindow.axaml.cs @@ -0,0 +1,467 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Platform; +using FluentAvalonia.UI.Controls; +using LanMountainDesktop.PluginSdk; +using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; +using LanMountainDesktop.ViewModels; +using Symbol = FluentIcons.Common.Symbol; + +namespace LanMountainDesktop.Views; + +public partial class SettingsWindow : Window, ISettingsPageHostContext +{ + private readonly ISettingsPageRegistry _pageRegistry; + private readonly IHostApplicationLifecycle _hostApplicationLifecycle; + private readonly Dictionary _cachedPages = new(StringComparer.OrdinalIgnoreCase); + private readonly bool _useSystemChrome; + + public SettingsWindow() + : this( + new SettingsWindowViewModel(), + EmptySettingsPageRegistry.Instance, + new HostApplicationLifecycleService()) + { + } + + public SettingsWindow( + SettingsWindowViewModel viewModel, + ISettingsPageRegistry pageRegistry, + IHostApplicationLifecycle hostApplicationLifecycle, + bool useSystemChrome = false) + { + _useSystemChrome = useSystemChrome; + ViewModel = viewModel; + _pageRegistry = pageRegistry; + _hostApplicationLifecycle = hostApplicationLifecycle; + DataContext = ViewModel; + InitializeComponent(); + ApplyChromeMode(useSystemChrome); + + Opened += OnOpened; + SizeChanged += OnWindowSizeChanged; + Closed += OnClosed; + Loaded += OnLoaded; + PendingRestartStateService.StateChanged += OnPendingRestartStateChanged; + } + + public SettingsWindowViewModel ViewModel { get; } + + private void OnLoaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + SyncPendingRestartState(); + SyncTitleText(); + UpdateChromeMetrics(); + UpdatePaneToggleIcon(); + } + + public void ReloadPages(string? pageId) + { + ViewModel.Pages.Clear(); + foreach (var page in _pageRegistry.GetPages().Where(page => !page.HideDefault)) + { + ViewModel.Pages.Add(page); + } + + _cachedPages.Clear(); + CloseDrawer(); + RebuildNavigationItems(); + NavigateTo(pageId ?? ViewModel.Pages.FirstOrDefault()?.PageId); + } + + public void OpenDrawer(Control content, string? title = null) + { + if (DrawerContentHost is null) + { + return; + } + + DrawerContentHost.Content = content; + ViewModel.DrawerTitle = title ?? ViewModel.DrawerFallbackTitle; + ViewModel.IsDrawerOpen = true; + SyncTitleText(); + } + + public void CloseDrawer() + { + if (DrawerContentHost is not null) + { + DrawerContentHost.Content = null; + } + + ViewModel.IsDrawerOpen = false; + ViewModel.DrawerTitle = null; + SyncTitleText(); + } + + public void RequestRestart(string? reason = null) + { + ViewModel.RestartMessage = string.IsNullOrWhiteSpace(reason) + ? ViewModel.GetDefaultRestartMessage() + : reason; + ViewModel.IsRestartRequested = true; + } + + public void ApplyChromeMode(bool useSystemChrome) + { + if (useSystemChrome || OperatingSystem.IsMacOS()) + { + ExtendClientAreaToDecorationsHint = true; + ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome; + ExtendClientAreaTitleBarHeightHint = -1; + SystemDecorations = SystemDecorations.Full; + return; + } + + SystemDecorations = SystemDecorations.BorderOnly; + ExtendClientAreaToDecorationsHint = true; + ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome; + ExtendClientAreaTitleBarHeightHint = 48; + } + + public void RefreshShellText() + { + SyncPendingRestartState(); + SyncTitleText(); + } + + private void RebuildNavigationItems() + { + if (RootNavigationView is null) + { + return; + } + + RootNavigationView.MenuItems.Clear(); + SettingsPageCategory? previousCategory = null; + + foreach (var page in ViewModel.Pages) + { + if (previousCategory is not null && previousCategory != page.Category) + { + RootNavigationView.MenuItems.Add(new NavigationViewItemSeparator()); + } + + RootNavigationView.MenuItems.Add(new NavigationViewItem + { + Content = page.Title, + Tag = page.PageId, + IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource + { + Symbol = MapIcon(page.IconKey), + IconVariant = FluentIcons.Common.IconVariant.Regular + } + }); + + previousCategory = page.Category; + } + } + + private void OnNavigationSelectionChanged(object? sender, NavigationViewSelectionChangedEventArgs e) + { + var selectedItem = e.SelectedItemContainer ?? e.SelectedItem as NavigationViewItem; + NavigateTo(selectedItem?.Tag as string); + } + + private void NavigateTo(string? pageId) + { + var descriptor = ResolveDescriptor(pageId); + if (descriptor is null) + { + return; + } + + var page = GetOrCreatePage(descriptor); + if (page is SettingsPageBase settingsPage) + { + settingsPage.InitializeHostContext(this); + settingsPage.NavigationUri = new Uri($"lmd://settings/{descriptor.PageId}", UriKind.Absolute); + settingsPage.OnNavigatedTo(null); + } + + if (ContentFrame is not null) + { + ContentFrame.Content = page; + } + + ViewModel.CurrentPageTitle = descriptor.Title; + ViewModel.CurrentPageDescription = descriptor.Description; + ViewModel.CurrentPageId = descriptor.PageId; + TrySelectNavigationItem(descriptor.PageId); + SyncTitleText(); + } + + private SettingsPageDescriptor? ResolveDescriptor(string? pageId) + { + if (!string.IsNullOrWhiteSpace(pageId) && + _pageRegistry.TryGetPage(pageId, out var descriptor) && + descriptor is not null) + { + return descriptor; + } + + return ViewModel.Pages.FirstOrDefault(); + } + + private Control GetOrCreatePage(SettingsPageDescriptor descriptor) + { + if (_cachedPages.TryGetValue(descriptor.PageId, out var page)) + { + return page; + } + + page = descriptor.CreatePage(this); + if (page is SettingsPageBase settingsPage) + { + settingsPage.InitializeHostContext(this); + } + + _cachedPages[descriptor.PageId] = page; + return page; + } + + private void TrySelectNavigationItem(string pageId) + { + if (RootNavigationView is null) + { + return; + } + + foreach (var item in RootNavigationView.MenuItems.OfType()) + { + if (string.Equals(item.Tag as string, pageId, StringComparison.OrdinalIgnoreCase)) + { + RootNavigationView.SelectedItem = item; + return; + } + } + } + + private async void OnRestartNowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + _ = sender; + _ = e; + _hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest( + Source: "SettingsWindow", + Reason: "User accepted restart from settings window.")); + await Task.CompletedTask; + } + + private void OnCloseDrawerClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + _ = sender; + _ = e; + CloseDrawer(); + } + + private void OnPendingRestartStateChanged() + { + SyncPendingRestartState(); + } + + private void SyncPendingRestartState() + { + if (!PendingRestartStateService.HasPendingRestart && !ViewModel.IsRestartRequested) + { + return; + } + + if (PendingRestartStateService.HasPendingRestart && string.IsNullOrWhiteSpace(ViewModel.RestartMessage)) + { + ViewModel.RestartMessage = ViewModel.GetDefaultRestartMessage(); + } + + ViewModel.IsRestartRequested = ViewModel.IsRestartRequested || PendingRestartStateService.HasPendingRestart; + } + + private void OnOpened(object? sender, EventArgs e) + { + _ = sender; + _ = e; + UpdateChromeMetrics(); + } + + private void OnWindowSizeChanged(object? sender, SizeChangedEventArgs e) + { + _ = sender; + _ = e; + UpdateChromeMetrics(); + } + + private void OnClosed(object? sender, EventArgs e) + { + _cachedPages.Clear(); + PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged; + Opened -= OnOpened; + SizeChanged -= OnWindowSizeChanged; + } + + private void OnWindowTitleBarPointerPressed(object? sender, PointerPressedEventArgs e) + { + _ = sender; + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + { + BeginMoveDrag(e); + } + } + + private void OnTogglePaneButtonClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + _ = sender; + _ = e; + if (RootNavigationView is null) + { + return; + } + + RootNavigationView.IsPaneOpen = !RootNavigationView.IsPaneOpen; + UpdatePaneToggleIcon(); + } + + private void OnCloseWindowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + _ = sender; + _ = e; + Close(); + } + + private void UpdatePaneToggleIcon() + { + if (TogglePaneButtonIcon is null || RootNavigationView is null) + { + return; + } + + TogglePaneButtonIcon.Icon = RootNavigationView.IsPaneOpen + ? FluentIcons.Common.Icon.PanelLeftContract + : FluentIcons.Common.Icon.PanelLeftExpand; + } + + private void UpdateChromeMetrics() + { + if (_useSystemChrome) + { + if (WindowTitleBarHost is { }) + { + WindowTitleBarHost.IsVisible = false; + } + return; + } + + if (WindowTitleBarHost is null || + TogglePaneButton is null || + TogglePaneButtonIcon is null || + WindowBrandIcon is null || + WindowTitleTextBlock is null || + RestartNowButton is null || + RestartButtonIcon is null || + RestartButtonTextBlock is null || + CloseWindowButton is null || + CloseWindowButtonIcon is null || + DrawerTitleTextBlock is null || + RootNavigationView is null) + { + return; + } + + var width = Bounds.Width > 1 ? Bounds.Width : Math.Max(Width, MinWidth); + var height = Bounds.Height > 1 ? Bounds.Height : Math.Max(Height, MinHeight); + var layoutScale = Math.Clamp(Math.Min(width / 1120d, height / 760d), 0.90, 1.18); + + var titleBarHeight = Math.Clamp(48d * layoutScale, 44d, 58d); + var titleBarButtonWidth = Math.Clamp(40d * layoutScale, 36d, 48d); + var titleBarButtonHeight = Math.Clamp(32d * layoutScale, 30d, 38d); + var titleFontSize = Math.Clamp(12d * layoutScale, 11d, 14d); + var titleBarIconSize = Math.Clamp(16d * layoutScale, 15d, 20d); + var drawerTitleFontSize = Math.Clamp(16d * layoutScale, 14d, 20d); + var chromePadding = Math.Clamp(12d * layoutScale, 10d, 18d); + var restartSpacing = Math.Clamp(6d * layoutScale, 6d, 10d); + + ExtendClientAreaTitleBarHeightHint = titleBarHeight; + + WindowTitleBarHost.Height = titleBarHeight; + WindowTitleBarHost.Padding = new Thickness(chromePadding, 0, chromePadding, 0); + + TogglePaneButton.Width = titleBarButtonWidth; + TogglePaneButton.Height = titleBarButtonHeight; + TogglePaneButtonIcon.FontSize = titleBarIconSize; + WindowBrandIcon.FontSize = titleBarIconSize + 2; + + WindowTitleTextBlock.FontSize = titleFontSize; + + RestartNowButton.Padding = new Thickness(chromePadding * 0.9, Math.Max(6, chromePadding * 0.5)); + if (RestartNowButton.Content is StackPanel restartStack) + { + restartStack.Spacing = restartSpacing; + } + + RestartButtonIcon.FontSize = titleBarIconSize; + RestartButtonTextBlock.FontSize = titleFontSize; + + CloseWindowButton.Width = titleBarButtonWidth; + CloseWindowButton.Height = titleBarButtonHeight; + CloseWindowButtonIcon.FontSize = titleBarIconSize; + + DrawerTitleTextBlock.FontSize = drawerTitleFontSize; + + RootNavigationView.OpenPaneLength = Math.Clamp(283d * layoutScale, 248d, 320d); + } + + private void SyncTitleText() + { + Title = ViewModel.Title; + + if (_useSystemChrome) + { + return; + } + + if (WindowTitleTextBlock is null || + DrawerTitleTextBlock is null) + { + return; + } + + WindowTitleTextBlock.Text = ViewModel.Title; + DrawerTitleTextBlock.IsVisible = !string.IsNullOrWhiteSpace(ViewModel.DrawerTitle); + } + + private sealed class EmptySettingsPageRegistry : ISettingsPageRegistry + { + public static EmptySettingsPageRegistry Instance { get; } = new(); + + public void Rebuild() + { + } + + public IReadOnlyList GetPages() + { + return Array.Empty(); + } + + public bool TryGetPage(string pageId, out SettingsPageDescriptor? descriptor) + { + descriptor = null; + return false; + } + } + + private static Symbol MapIcon(string iconKey) + { + return iconKey?.Trim() switch + { + "DesignIdeas" => Symbol.Color, + "GridDots" => Symbol.GridDots, + "PuzzlePiece" => Symbol.PuzzlePiece, + "Info" => Symbol.Info, + "ArrowSync" => Symbol.ArrowSync, + _ => Symbol.Settings + }; + } +} diff --git a/LanMountainDesktop/plugins/PluginLoader.cs b/LanMountainDesktop/plugins/PluginLoader.cs index 5baae0b..1a4252a 100644 --- a/LanMountainDesktop/plugins/PluginLoader.cs +++ b/LanMountainDesktop/plugins/PluginLoader.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; using LanMountainDesktop.PluginSdk; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -310,10 +311,15 @@ public sealed class PluginLoader services.AddSingleton(runtimeContext.Manifest); services.AddSingleton>(runtimeContext.Properties); services.AddSingleton(); + services.AddSingleton(provider => + new PluginScopedSettingsService( + runtimeContext.Manifest.Id, + provider.GetRequiredService())); RegisterHostService(services, hostServices); RegisterHostService(services, hostServices); RegisterHostService(services, hostServices); + RegisterHostService(services, hostServices); RegisterHostService(services, hostServices); RegisterHostService(services, hostServices); diff --git a/LanMountainDesktop/plugins/PluginRuntimeService.cs b/LanMountainDesktop/plugins/PluginRuntimeService.cs index f7c15b3..e33976a 100644 --- a/LanMountainDesktop/plugins/PluginRuntimeService.cs +++ b/LanMountainDesktop/plugins/PluginRuntimeService.cs @@ -55,6 +55,7 @@ public sealed class PluginRuntimeService : IDisposable _packageManager, _applicationLifecycle, _exportRegistry, + _settingsFacade, _settingsFacade.Settings, _settingsFacade.Catalog); _loaderOptions = CreateOptions(); @@ -824,6 +825,7 @@ public sealed class PluginRuntimeService : IDisposable private readonly IPluginPackageManager _packageManager; private readonly IHostApplicationLifecycle _applicationLifecycle; private readonly IPluginExportRegistry _exportRegistry; + private readonly ISettingsFacadeService _settingsFacade; private readonly ISettingsService _settingsService; private readonly ISettingsCatalog _settingsCatalog; @@ -831,12 +833,14 @@ public sealed class PluginRuntimeService : IDisposable IPluginPackageManager packageManager, IHostApplicationLifecycle applicationLifecycle, IPluginExportRegistry exportRegistry, + ISettingsFacadeService settingsFacade, ISettingsService settingsService, ISettingsCatalog settingsCatalog) { _packageManager = packageManager; _applicationLifecycle = applicationLifecycle; _exportRegistry = exportRegistry; + _settingsFacade = settingsFacade; _settingsService = settingsService; _settingsCatalog = settingsCatalog; } @@ -858,6 +862,11 @@ public sealed class PluginRuntimeService : IDisposable return _exportRegistry; } + if (serviceType == typeof(ISettingsFacadeService)) + { + return _settingsFacade; + } + if (serviceType == typeof(ISettingsService)) { return _settingsService;