From c4df2436100c5c393c02350165e21f829e732b78 Mon Sep 17 00:00:00 2001 From: lincube Date: Fri, 13 Mar 2026 00:33:00 +0800 Subject: [PATCH] setting_re2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 设置架构革新中 --- .../SamplePlugin.cs | 18 +- .../IComponentSettingsAccessor.cs | 24 + .../ISettingsCatalog.cs | 10 + .../ISettingsService.cs | 56 + .../PluginServiceCollectionExtensions.cs | 20 +- .../PluginSettingsPageRegistration.cs | 39 - .../PluginSettingsSectionBuilder.cs | 147 +++ .../PluginSettingsSectionRegistration.cs | 40 + .../SettingsCategories.cs | 14 + .../SettingsChangedEvent.cs | 32 + .../SettingsOptionChoice.cs | 17 + .../SettingsOptionDefinition.cs | 53 + .../SettingsOptionType.cs | 11 + LanMountainDesktop.PluginSdk/SettingsScope.cs | 9 + .../SettingsSectionDefinition.cs | 53 + LanMountainDesktop/App.axaml.cs | 18 +- .../Models/AppSettingsSnapshot.cs | 3 +- LanMountainDesktop/Models/TaskbarActionId.cs | 4 +- LanMountainDesktop/Models/TaskbarContext.cs | 8 +- .../Services/DesktopGridLayoutService.cs | 116 ++ .../IndependentSettingsModuleService.cs | 88 -- .../Settings/SettingsCatalogService.cs | 94 ++ .../Services/Settings/SettingsContracts.cs | 189 +++ .../Settings/SettingsDomainServices.cs | 627 ++++++++++ .../Services/Settings/SettingsService.cs | 423 +++++++ .../AnalogClockWidgetSettingsWindow.axaml | 73 -- .../AnalogClockWidgetSettingsWindow.axaml.cs | 230 ---- .../BaiduHotSearchSettingsWindow.axaml | 110 -- .../BaiduHotSearchSettingsWindow.axaml.cs | 193 --- .../BilibiliHotSearchSettingsWindow.axaml | 87 -- .../BilibiliHotSearchSettingsWindow.axaml.cs | 153 --- .../ClassScheduleSettingsWindow.axaml | 61 - .../ClassScheduleSettingsWindow.axaml.cs | 369 ------ .../CnrDailyNewsSettingsWindow.axaml | 87 -- .../CnrDailyNewsSettingsWindow.axaml.cs | 153 --- .../DailyArtworkSettingsWindow.axaml | 54 - .../DailyArtworkSettingsWindow.axaml.cs | 117 -- .../Components/DailyWordSettingsWindow.axaml | 87 -- .../DailyWordSettingsWindow.axaml.cs | 153 --- .../Components/DateWidgetSettingsWindow.axaml | 20 - .../DateWidgetSettingsWindow.axaml.cs | 11 - .../Components/IfengNewsSettingsWindow.axaml | 113 -- .../IfengNewsSettingsWindow.axaml.cs | 194 --- .../Stcn24ForumSettingsWindow.axaml | 128 -- .../Stcn24ForumSettingsWindow.axaml.cs | 199 ---- ...StudyEnvironmentWidgetSettingsWindow.axaml | 50 - ...dyEnvironmentWidgetSettingsWindow.axaml.cs | 92 -- .../WeatherWidgetSettingsWindow.axaml | 87 -- .../WeatherWidgetSettingsWindow.axaml.cs | 173 --- .../WorldClockWidgetSettingsWindow.axaml | 121 -- .../WorldClockWidgetSettingsWindow.axaml.cs | 268 ----- .../IndependentSettingsModuleWindowBase.cs | 23 - .../Views/IndependentSettingsPageCategory.cs | 9 - .../IndependentSettingsPageDefinition.cs | 12 - .../Views/MainWindow.ComponentSystem.cs | 655 +--------- .../Views/MainWindow.DesktopPaging.cs | 2 +- .../Views/MainWindow.Settings.cs | 7 +- LanMountainDesktop/Views/MainWindow.axaml | 64 - LanMountainDesktop/Views/MainWindow.axaml.cs | 163 +-- .../Views/SettingsWindow.ComponentsSummary.cs | 78 -- .../Views/SettingsWindow.Controls.cs | 261 ---- .../Views/SettingsWindow.Core.cs | 857 -------------- .../Views/SettingsWindow.Layout.cs | 524 -------- .../Views/SettingsWindow.Localization.cs | 317 ----- .../Views/SettingsWindow.RenderBackend.cs | 42 - .../Views/SettingsWindow.RestartPrompt.cs | 115 -- .../Views/SettingsWindow.State.cs | 76 -- .../Views/SettingsWindow.Update.cs | 339 ------ .../Views/SettingsWindow.WallpaperTheme.cs | 1052 ----------------- .../Views/SettingsWindow.WeatherLauncher.cs | 1006 ---------------- LanMountainDesktop/Views/SettingsWindow.axaml | 360 ------ .../Views/SettingsWindow.axaml.cs | 606 ---------- LanMountainDesktop/plugins/LoadedPlugin.cs | 6 +- .../plugins/MainWindow.PluginSettingsHost.cs | 151 +-- .../plugins/PluginContributions.cs | 4 +- LanMountainDesktop/plugins/PluginLoader.cs | 14 +- .../plugins/PluginMarketCacheService.cs | 2 +- .../plugins/PluginMarketEmbeddedView.cs | 2 +- .../plugins/PluginMarketIconService.cs | 2 +- .../plugins/PluginMarketIndexService.cs | 2 +- .../plugins/PluginMarketInstallService.cs | 2 +- .../plugins/PluginMarketModels.cs | 2 +- .../plugins/PluginMarketReadmeService.cs | 2 +- .../PluginMarketReleaseResolverService.cs | 2 +- .../plugins/PluginMarketSettingsPage.axaml.cs | 1 + .../plugins/PluginRuntimeService.cs | 61 +- .../plugins/PluginSettingsPage.Host.cs | 7 +- .../plugins/PluginSharedContractManager.cs | 2 +- ...Window.PluginMarketSettingsLocalization.cs | 9 - .../SettingsWindow.PluginSettingsControls.cs | 15 - .../SettingsWindow.PluginSettingsHost.cs | 198 ---- ...ttingsWindow.PluginSettingsLocalization.cs | 20 - 92 files changed, 2048 insertions(+), 10520 deletions(-) create mode 100644 LanMountainDesktop.PluginSdk/IComponentSettingsAccessor.cs create mode 100644 LanMountainDesktop.PluginSdk/ISettingsCatalog.cs create mode 100644 LanMountainDesktop.PluginSdk/ISettingsService.cs delete mode 100644 LanMountainDesktop.PluginSdk/PluginSettingsPageRegistration.cs create mode 100644 LanMountainDesktop.PluginSdk/PluginSettingsSectionBuilder.cs create mode 100644 LanMountainDesktop.PluginSdk/PluginSettingsSectionRegistration.cs create mode 100644 LanMountainDesktop.PluginSdk/SettingsCategories.cs create mode 100644 LanMountainDesktop.PluginSdk/SettingsChangedEvent.cs create mode 100644 LanMountainDesktop.PluginSdk/SettingsOptionChoice.cs create mode 100644 LanMountainDesktop.PluginSdk/SettingsOptionDefinition.cs create mode 100644 LanMountainDesktop.PluginSdk/SettingsOptionType.cs create mode 100644 LanMountainDesktop.PluginSdk/SettingsScope.cs create mode 100644 LanMountainDesktop.PluginSdk/SettingsSectionDefinition.cs create mode 100644 LanMountainDesktop/Services/DesktopGridLayoutService.cs delete mode 100644 LanMountainDesktop/Services/IndependentSettingsModuleService.cs create mode 100644 LanMountainDesktop/Services/Settings/SettingsCatalogService.cs create mode 100644 LanMountainDesktop/Services/Settings/SettingsContracts.cs create mode 100644 LanMountainDesktop/Services/Settings/SettingsDomainServices.cs create mode 100644 LanMountainDesktop/Services/Settings/SettingsService.cs delete mode 100644 LanMountainDesktop/Views/Components/AnalogClockWidgetSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/AnalogClockWidgetSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/BaiduHotSearchSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/BaiduHotSearchSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/ClassScheduleSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/ClassScheduleSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/DateWidgetSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/DateWidgetSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/IfengNewsSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/IfengNewsSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/StudyEnvironmentWidgetSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/StudyEnvironmentWidgetSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/Components/WorldClockWidgetSettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/Components/WorldClockWidgetSettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/Views/IndependentSettingsModuleWindowBase.cs delete mode 100644 LanMountainDesktop/Views/IndependentSettingsPageCategory.cs delete mode 100644 LanMountainDesktop/Views/IndependentSettingsPageDefinition.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.ComponentsSummary.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.Controls.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.Core.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.Layout.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.Localization.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.RenderBackend.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.RestartPrompt.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.State.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.Update.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.WallpaperTheme.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.WeatherLauncher.cs delete mode 100644 LanMountainDesktop/Views/SettingsWindow.axaml delete mode 100644 LanMountainDesktop/Views/SettingsWindow.axaml.cs delete mode 100644 LanMountainDesktop/plugins/SettingsWindow.PluginMarketSettingsLocalization.cs delete mode 100644 LanMountainDesktop/plugins/SettingsWindow.PluginSettingsControls.cs delete mode 100644 LanMountainDesktop/plugins/SettingsWindow.PluginSettingsHost.cs delete mode 100644 LanMountainDesktop/plugins/SettingsWindow.PluginSettingsLocalization.cs diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePlugin.cs b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePlugin.cs index f056e07..58e11f0 100644 --- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePlugin.cs +++ b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePlugin.cs @@ -16,7 +16,6 @@ public sealed class SamplePlugin : PluginBase, IDisposable var hostName = GetHostProperty(context, PluginHostPropertyKeys.HostApplicationName, "UnknownHost"); var hostVersion = GetHostProperty(context, PluginHostPropertyKeys.HostVersion, "UnknownVersion"); var sdkApiVersion = GetHostProperty(context, PluginHostPropertyKeys.PluginSdkApiVersion, "UnknownApiVersion"); - var hostApplicationLifecycle = context.GetService(); var messageBus = context.GetService() ?? throw new InvalidOperationException("Plugin message bus is not available."); @@ -44,31 +43,26 @@ public sealed class SamplePlugin : PluginBase, IDisposable File.AppendAllText(logPath, initMessage + Environment.NewLine); _stateService.MarkBackendReady(localizer.Format( "status.backend.detail.log_written", - "初始化日志已写入:{0}", + "Initialization log written: {0}", logPath)); } catch (Exception ex) { _stateService.MarkBackendFaulted(localizer.Format( "status.backend.detail.log_write_failed", - "初始化日志写入失败:{0}", + "Initialization log failed: {0}", ex.Message)); throw; } _clockService.Start(); - context.RegisterSettingsPage(new PluginSettingsPageRegistration( - "status", - localizer.GetString("settings.page_title", "插件状态"), - () => new SamplePluginSettingsView(context))); - context.RegisterDesktopComponent(new PluginDesktopComponentRegistration( "LanMountainDesktop.SamplePlugin.StatusClock", - localizer.GetString("widget.display_name", "示例插件状态时钟"), + localizer.GetString("widget.display_name", "Sample Plugin Status Clock"), widgetContext => new SamplePluginStatusClockWidget(widgetContext), iconKey: "PuzzlePiece", - category: localizer.GetString("widget.category", "插件"), + category: localizer.GetString("widget.category", "Plugins"), minWidthCells: 4, minHeightCells: 4, allowDesktopPlacement: true, @@ -78,10 +72,10 @@ public sealed class SamplePlugin : PluginBase, IDisposable context.RegisterDesktopComponent(new PluginDesktopComponentRegistration( "LanMountainDesktop.SamplePlugin.CloseDesktop", - localizer.GetString("widget.close_desktop.display_name", "关闭桌面"), + localizer.GetString("widget.close_desktop.display_name", "Close Desktop"), widgetContext => new SamplePluginCloseDesktopWidget(widgetContext), iconKey: "DismissCircle", - category: localizer.GetString("widget.category", "鎻掍欢"), + category: localizer.GetString("widget.category", "Plugins"), minWidthCells: 2, minHeightCells: 1, allowDesktopPlacement: true, diff --git a/LanMountainDesktop.PluginSdk/IComponentSettingsAccessor.cs b/LanMountainDesktop.PluginSdk/IComponentSettingsAccessor.cs new file mode 100644 index 0000000..41f9788 --- /dev/null +++ b/LanMountainDesktop.PluginSdk/IComponentSettingsAccessor.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace LanMountainDesktop.PluginSdk; + +public interface IComponentSettingsAccessor +{ + string ComponentId { get; } + + string? PlacementId { get; } + + T LoadSnapshot() where T : new(); + + void SaveSnapshot(T snapshot, IReadOnlyCollection? changedKeys = null); + + T LoadSection(string sectionId) where T : new(); + + void SaveSection(string sectionId, T section, IReadOnlyCollection? changedKeys = null); + + void DeleteSection(string sectionId); + + T? GetValue(string key); + + void SetValue(string key, T value, IReadOnlyCollection? changedKeys = null); +} diff --git a/LanMountainDesktop.PluginSdk/ISettingsCatalog.cs b/LanMountainDesktop.PluginSdk/ISettingsCatalog.cs new file mode 100644 index 0000000..61efbd0 --- /dev/null +++ b/LanMountainDesktop.PluginSdk/ISettingsCatalog.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace LanMountainDesktop.PluginSdk; + +public interface ISettingsCatalog +{ + IReadOnlyList GetSections(); + + IReadOnlyList GetSections(SettingsScope scope); +} diff --git a/LanMountainDesktop.PluginSdk/ISettingsService.cs b/LanMountainDesktop.PluginSdk/ISettingsService.cs new file mode 100644 index 0000000..0c9ede1 --- /dev/null +++ b/LanMountainDesktop.PluginSdk/ISettingsService.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; + +namespace LanMountainDesktop.PluginSdk; + +public interface ISettingsService +{ + event EventHandler? Changed; + + T LoadSnapshot(SettingsScope scope, string? subjectId = null, string? placementId = null) where T : new(); + + void SaveSnapshot( + SettingsScope scope, + T snapshot, + string? subjectId = null, + string? placementId = null, + string? sectionId = null, + IReadOnlyCollection? changedKeys = null); + + T LoadSection( + SettingsScope scope, + string subjectId, + string sectionId, + string? placementId = null) where T : new(); + + void SaveSection( + SettingsScope scope, + string subjectId, + string sectionId, + T section, + string? placementId = null, + IReadOnlyCollection? changedKeys = null); + + void DeleteSection( + SettingsScope scope, + string subjectId, + string sectionId, + string? placementId = null); + + T? GetValue( + SettingsScope scope, + string key, + string? subjectId = null, + string? placementId = null, + string? sectionId = null); + + void SetValue( + SettingsScope scope, + string key, + T value, + string? subjectId = null, + string? placementId = null, + string? sectionId = null, + IReadOnlyCollection? changedKeys = null); + + IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId); +} diff --git a/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs b/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs index 944233d..322997c 100644 --- a/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs +++ b/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs @@ -5,20 +5,26 @@ namespace LanMountainDesktop.PluginSdk; public static class PluginServiceCollectionExtensions { - public static IServiceCollection AddPluginSettingsPage( + public static IServiceCollection AddPluginSettingsSection( this IServiceCollection services, string id, - string title, + string titleLocalizationKey, + Action configure, + string? descriptionLocalizationKey = null, + string iconKey = "PuzzlePiece", int sortOrder = 0) - where TControl : Control { ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configure); - services.AddSingleton(new PluginSettingsPageRegistration( + var builder = new PluginSettingsSectionBuilder( id, - title, - provider => ActivatorUtilities.CreateInstance(provider), - sortOrder)); + titleLocalizationKey, + descriptionLocalizationKey, + iconKey, + sortOrder); + configure(builder); + services.AddSingleton(builder.Build()); return services; } diff --git a/LanMountainDesktop.PluginSdk/PluginSettingsPageRegistration.cs b/LanMountainDesktop.PluginSdk/PluginSettingsPageRegistration.cs deleted file mode 100644 index 1db33bd..0000000 --- a/LanMountainDesktop.PluginSdk/PluginSettingsPageRegistration.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Avalonia.Controls; - -namespace LanMountainDesktop.PluginSdk; - -public sealed class PluginSettingsPageRegistration -{ - public PluginSettingsPageRegistration( - string id, - string title, - Func contentFactory, - int sortOrder = 0) - { - ArgumentException.ThrowIfNullOrWhiteSpace(id); - ArgumentException.ThrowIfNullOrWhiteSpace(title); - ArgumentNullException.ThrowIfNull(contentFactory); - - Id = id.Trim(); - Title = title.Trim(); - ContentFactory = contentFactory; - SortOrder = sortOrder; - } - - public PluginSettingsPageRegistration( - string id, - string title, - Func contentFactory, - int sortOrder = 0) - : this(id, title, _ => contentFactory(), sortOrder) - { - } - - public string Id { get; } - - public string Title { get; } - - public int SortOrder { get; } - - public Func ContentFactory { get; } -} diff --git a/LanMountainDesktop.PluginSdk/PluginSettingsSectionBuilder.cs b/LanMountainDesktop.PluginSdk/PluginSettingsSectionBuilder.cs new file mode 100644 index 0000000..878a7c7 --- /dev/null +++ b/LanMountainDesktop.PluginSdk/PluginSettingsSectionBuilder.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; + +namespace LanMountainDesktop.PluginSdk; + +public sealed class PluginSettingsSectionBuilder +{ + private readonly List _options = []; + + internal PluginSettingsSectionBuilder( + string id, + string titleLocalizationKey, + string? descriptionLocalizationKey, + string iconKey, + int sortOrder) + { + Id = id; + TitleLocalizationKey = titleLocalizationKey; + DescriptionLocalizationKey = descriptionLocalizationKey; + IconKey = iconKey; + SortOrder = sortOrder; + } + + public string Id { get; } + + public string TitleLocalizationKey { get; } + + public string? DescriptionLocalizationKey { get; } + + public string IconKey { get; } + + public int SortOrder { get; } + + public IReadOnlyList Options => _options; + + public PluginSettingsSectionBuilder AddOption(SettingsOptionDefinition option) + { + ArgumentNullException.ThrowIfNull(option); + _options.Add(option); + return this; + } + + public PluginSettingsSectionBuilder AddToggle( + string key, + string titleLocalizationKey, + string? descriptionLocalizationKey = null, + bool defaultValue = false) + { + return AddOption(new SettingsOptionDefinition( + key, + SettingsOptionType.Toggle, + titleLocalizationKey, + descriptionLocalizationKey, + defaultValue)); + } + + public PluginSettingsSectionBuilder AddText( + string key, + string titleLocalizationKey, + string? descriptionLocalizationKey = null, + string defaultValue = "", + string? validationPattern = null) + { + return AddOption(new SettingsOptionDefinition( + key, + SettingsOptionType.Text, + titleLocalizationKey, + descriptionLocalizationKey, + defaultValue, + validationPattern: validationPattern)); + } + + public PluginSettingsSectionBuilder AddNumber( + string key, + string titleLocalizationKey, + string? descriptionLocalizationKey = null, + double defaultValue = 0, + double? minimum = null, + double? maximum = null) + { + return AddOption(new SettingsOptionDefinition( + key, + SettingsOptionType.Number, + titleLocalizationKey, + descriptionLocalizationKey, + defaultValue, + minimum: minimum, + maximum: maximum)); + } + + public PluginSettingsSectionBuilder AddSelect( + string key, + string titleLocalizationKey, + IEnumerable choices, + string? descriptionLocalizationKey = null, + string? defaultValue = null) + { + ArgumentNullException.ThrowIfNull(choices); + var normalizedChoices = choices.ToArray(); + + return AddOption(new SettingsOptionDefinition( + key, + SettingsOptionType.Select, + titleLocalizationKey, + descriptionLocalizationKey, + defaultValue, + normalizedChoices)); + } + + public PluginSettingsSectionBuilder AddPath( + string key, + string titleLocalizationKey, + string? descriptionLocalizationKey = null, + string defaultValue = "") + { + return AddOption(new SettingsOptionDefinition( + key, + SettingsOptionType.Path, + titleLocalizationKey, + descriptionLocalizationKey, + defaultValue)); + } + + public PluginSettingsSectionBuilder AddList( + string key, + string titleLocalizationKey, + string? descriptionLocalizationKey = null, + IReadOnlyList? defaultValue = null) + { + return AddOption(new SettingsOptionDefinition( + key, + SettingsOptionType.List, + titleLocalizationKey, + descriptionLocalizationKey, + defaultValue ?? Array.Empty())); + } + + internal PluginSettingsSectionRegistration Build() + { + return new PluginSettingsSectionRegistration( + Id, + TitleLocalizationKey, + _options.ToArray(), + DescriptionLocalizationKey, + IconKey, + SortOrder); + } +} diff --git a/LanMountainDesktop.PluginSdk/PluginSettingsSectionRegistration.cs b/LanMountainDesktop.PluginSdk/PluginSettingsSectionRegistration.cs new file mode 100644 index 0000000..3bc1676 --- /dev/null +++ b/LanMountainDesktop.PluginSdk/PluginSettingsSectionRegistration.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace LanMountainDesktop.PluginSdk; + +public sealed class PluginSettingsSectionRegistration +{ + public PluginSettingsSectionRegistration( + string id, + string titleLocalizationKey, + IReadOnlyList options, + string? descriptionLocalizationKey = null, + string iconKey = "PuzzlePiece", + int sortOrder = 0) + { + ArgumentException.ThrowIfNullOrWhiteSpace(id); + ArgumentException.ThrowIfNullOrWhiteSpace(titleLocalizationKey); + ArgumentException.ThrowIfNullOrWhiteSpace(iconKey); + + Id = id.Trim(); + TitleLocalizationKey = titleLocalizationKey.Trim(); + DescriptionLocalizationKey = string.IsNullOrWhiteSpace(descriptionLocalizationKey) + ? null + : descriptionLocalizationKey.Trim(); + IconKey = iconKey.Trim(); + SortOrder = sortOrder; + Options = options ?? []; + } + + public string Id { get; } + + public string TitleLocalizationKey { get; } + + public string? DescriptionLocalizationKey { get; } + + public string IconKey { get; } + + public int SortOrder { get; } + + public IReadOnlyList Options { get; } +} diff --git a/LanMountainDesktop.PluginSdk/SettingsCategories.cs b/LanMountainDesktop.PluginSdk/SettingsCategories.cs new file mode 100644 index 0000000..5d253ce --- /dev/null +++ b/LanMountainDesktop.PluginSdk/SettingsCategories.cs @@ -0,0 +1,14 @@ +namespace LanMountainDesktop.PluginSdk; + +public static class SettingsCategories +{ + public const string General = "General"; + public const string Appearance = "Appearance"; + public const string Components = "Components"; + public const string Plugins = "Plugins"; + public const string PluginMarket = "PluginMarket"; + public const string Update = "Update"; + public const string About = "About"; + public const string Advanced = "Advanced"; + public const string External = "External"; +} diff --git a/LanMountainDesktop.PluginSdk/SettingsChangedEvent.cs b/LanMountainDesktop.PluginSdk/SettingsChangedEvent.cs new file mode 100644 index 0000000..e86a596 --- /dev/null +++ b/LanMountainDesktop.PluginSdk/SettingsChangedEvent.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace LanMountainDesktop.PluginSdk; + +public sealed class SettingsChangedEvent +{ + public SettingsChangedEvent( + SettingsScope scope, + string? subjectId = null, + string? placementId = null, + string? sectionId = null, + IReadOnlyCollection? changedKeys = null) + { + Scope = scope; + SubjectId = string.IsNullOrWhiteSpace(subjectId) ? null : subjectId.Trim(); + PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim(); + SectionId = string.IsNullOrWhiteSpace(sectionId) ? null : sectionId.Trim(); + ChangedKeys = changedKeys is { Count: > 0 } + ? changedKeys.ToArray() + : []; + } + + public SettingsScope Scope { get; } + + public string? SubjectId { get; } + + public string? PlacementId { get; } + + public string? SectionId { get; } + + public IReadOnlyCollection ChangedKeys { get; } +} diff --git a/LanMountainDesktop.PluginSdk/SettingsOptionChoice.cs b/LanMountainDesktop.PluginSdk/SettingsOptionChoice.cs new file mode 100644 index 0000000..b4e9075 --- /dev/null +++ b/LanMountainDesktop.PluginSdk/SettingsOptionChoice.cs @@ -0,0 +1,17 @@ +namespace LanMountainDesktop.PluginSdk; + +public sealed class SettingsOptionChoice +{ + public SettingsOptionChoice(string value, string titleLocalizationKey) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + ArgumentException.ThrowIfNullOrWhiteSpace(titleLocalizationKey); + + Value = value.Trim(); + TitleLocalizationKey = titleLocalizationKey.Trim(); + } + + public string Value { get; } + + public string TitleLocalizationKey { get; } +} diff --git a/LanMountainDesktop.PluginSdk/SettingsOptionDefinition.cs b/LanMountainDesktop.PluginSdk/SettingsOptionDefinition.cs new file mode 100644 index 0000000..188956c --- /dev/null +++ b/LanMountainDesktop.PluginSdk/SettingsOptionDefinition.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +namespace LanMountainDesktop.PluginSdk; + +public sealed class SettingsOptionDefinition +{ + public SettingsOptionDefinition( + string key, + SettingsOptionType optionType, + string titleLocalizationKey, + string? descriptionLocalizationKey = null, + object? defaultValue = null, + IReadOnlyList? choices = null, + double? minimum = null, + double? maximum = null, + string? validationPattern = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(key); + ArgumentException.ThrowIfNullOrWhiteSpace(titleLocalizationKey); + + Key = key.Trim(); + OptionType = optionType; + TitleLocalizationKey = titleLocalizationKey.Trim(); + DescriptionLocalizationKey = string.IsNullOrWhiteSpace(descriptionLocalizationKey) + ? null + : descriptionLocalizationKey.Trim(); + DefaultValue = defaultValue; + Choices = choices ?? []; + Minimum = minimum; + Maximum = maximum; + ValidationPattern = string.IsNullOrWhiteSpace(validationPattern) + ? null + : validationPattern.Trim(); + } + + public string Key { get; } + + public SettingsOptionType OptionType { get; } + + public string TitleLocalizationKey { get; } + + public string? DescriptionLocalizationKey { get; } + + public object? DefaultValue { get; } + + public IReadOnlyList Choices { get; } + + public double? Minimum { get; } + + public double? Maximum { get; } + + public string? ValidationPattern { get; } +} diff --git a/LanMountainDesktop.PluginSdk/SettingsOptionType.cs b/LanMountainDesktop.PluginSdk/SettingsOptionType.cs new file mode 100644 index 0000000..a050567 --- /dev/null +++ b/LanMountainDesktop.PluginSdk/SettingsOptionType.cs @@ -0,0 +1,11 @@ +namespace LanMountainDesktop.PluginSdk; + +public enum SettingsOptionType +{ + Toggle = 0, + Select = 1, + Text = 2, + Number = 3, + Path = 4, + List = 5 +} diff --git a/LanMountainDesktop.PluginSdk/SettingsScope.cs b/LanMountainDesktop.PluginSdk/SettingsScope.cs new file mode 100644 index 0000000..fd103a5 --- /dev/null +++ b/LanMountainDesktop.PluginSdk/SettingsScope.cs @@ -0,0 +1,9 @@ +namespace LanMountainDesktop.PluginSdk; + +public enum SettingsScope +{ + App = 0, + Launcher = 1, + Plugin = 2, + ComponentInstance = 3 +} diff --git a/LanMountainDesktop.PluginSdk/SettingsSectionDefinition.cs b/LanMountainDesktop.PluginSdk/SettingsSectionDefinition.cs new file mode 100644 index 0000000..7143fbd --- /dev/null +++ b/LanMountainDesktop.PluginSdk/SettingsSectionDefinition.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +namespace LanMountainDesktop.PluginSdk; + +public sealed class SettingsSectionDefinition +{ + public SettingsSectionDefinition( + string id, + string category, + SettingsScope scope, + string titleLocalizationKey, + string? descriptionLocalizationKey = null, + string iconKey = "Settings", + int sortOrder = 0, + string? subjectId = null, + IReadOnlyList? options = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(id); + ArgumentException.ThrowIfNullOrWhiteSpace(category); + ArgumentException.ThrowIfNullOrWhiteSpace(titleLocalizationKey); + ArgumentException.ThrowIfNullOrWhiteSpace(iconKey); + + Id = id.Trim(); + Category = category.Trim(); + Scope = scope; + TitleLocalizationKey = titleLocalizationKey.Trim(); + DescriptionLocalizationKey = string.IsNullOrWhiteSpace(descriptionLocalizationKey) + ? null + : descriptionLocalizationKey.Trim(); + IconKey = iconKey.Trim(); + SortOrder = sortOrder; + SubjectId = string.IsNullOrWhiteSpace(subjectId) ? null : subjectId.Trim(); + Options = options ?? []; + } + + public string Id { get; } + + public string Category { get; } + + public SettingsScope Scope { get; } + + public string TitleLocalizationKey { get; } + + public string? DescriptionLocalizationKey { get; } + + public string IconKey { get; } + + public int SortOrder { get; } + + public string? SubjectId { get; } + + public IReadOnlyList Options { get; } +} diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index e6d9094..8148174 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -40,7 +40,6 @@ public partial class App : Application private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop; private ShutdownIntent _shutdownIntent; - private readonly IndependentSettingsModuleService _independentSettingsModuleService = new(); private TrayIcons? _trayIcons; private PluginRuntimeService? _pluginRuntimeService; private MainWindow? _mainWindow; @@ -55,7 +54,9 @@ public partial class App : Application internal void OpenIndependentSettingsModule(string source, string? pageTag = null) { - _independentSettingsModuleService.ShowOrActivate(source, pageTag); + AppLogger.Info( + "SettingsFacade", + $"Settings UI entry is disabled by hard-cut migration. Source='{source}'; PageTag='{pageTag ?? ""}'."); } public override void Initialize() @@ -105,11 +106,6 @@ public partial class App : Application RestoreOrCreateMainWindow(showSingleInstanceNotice: false, source: "TrayMenu"); } - private void OnTraySettingsClick(object? sender, EventArgs e) - { - OpenIndependentSettingsModule("TrayMenu"); - } - private void OnTrayRestartClick(object? sender, EventArgs e) { _ = _hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest( @@ -206,12 +202,6 @@ public partial class App : Application menu.Items.Add(new NativeMenuItemSeparator()); - var settingsItem = new NativeMenuItem(L("tray.menu.settings", "Settings")); - settingsItem.Click += OnTraySettingsClick; - menu.Items.Add(settingsItem); - - menu.Items.Add(new NativeMenuItemSeparator()); - var restartItem = new NativeMenuItem(L("tray.menu.restart", "Restart App")); restartItem.Click += OnTrayRestartClick; menu.Items.Add(restartItem); @@ -361,8 +351,6 @@ public partial class App : Application _exitCleanupCompleted = true; AppSettingsService.SettingsSaved -= OnAppSettingsSaved; - _independentSettingsModuleService.CloseIfOpen(); - try { _pluginRuntimeService?.Dispose(); diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs index 390f899..1c5c1eb 100644 --- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs @@ -60,8 +60,7 @@ public sealed class AppSettingsSnapshot public List PinnedTaskbarActions { get; set; } = [ - TaskbarActionId.MinimizeToWindows.ToString(), - TaskbarActionId.OpenSettings.ToString() + TaskbarActionId.MinimizeToWindows.ToString() ]; public bool EnableDynamicTaskbarActions { get; set; } = true; diff --git a/LanMountainDesktop/Models/TaskbarActionId.cs b/LanMountainDesktop/Models/TaskbarActionId.cs index c421bd6..0f1b434 100644 --- a/LanMountainDesktop/Models/TaskbarActionId.cs +++ b/LanMountainDesktop/Models/TaskbarActionId.cs @@ -1,12 +1,10 @@ -namespace LanMountainDesktop.Models; +namespace LanMountainDesktop.Models; public enum TaskbarActionId { MinimizeToWindows, - OpenSettings, AddDesktopPage, DeleteDesktopPage, DeleteComponent, - EditComponent, HideLauncherEntry } diff --git a/LanMountainDesktop/Models/TaskbarContext.cs b/LanMountainDesktop/Models/TaskbarContext.cs index 5e32c45..2974bae 100644 --- a/LanMountainDesktop/Models/TaskbarContext.cs +++ b/LanMountainDesktop/Models/TaskbarContext.cs @@ -2,11 +2,5 @@ public enum TaskbarContext { - Desktop, - SettingsWallpaper, - SettingsGrid, - SettingsColor, - SettingsStatusBar, - SettingsWeather, - SettingsRegion + Desktop } diff --git a/LanMountainDesktop/Services/DesktopGridLayoutService.cs b/LanMountainDesktop/Services/DesktopGridLayoutService.cs new file mode 100644 index 0000000..56986b6 --- /dev/null +++ b/LanMountainDesktop/Services/DesktopGridLayoutService.cs @@ -0,0 +1,116 @@ +using System; + +namespace LanMountainDesktop.Services; + +public readonly record struct DesktopGridMetrics( + int ColumnCount, + int RowCount, + double CellSize, + double GapPx, + double EdgeInsetPx, + double GridWidthPx, + double GridHeightPx) +{ + public double Pitch => CellSize + GapPx; +} + +public sealed class DesktopGridLayoutService +{ + public const string RelaxedSpacingPreset = "Relaxed"; + public const string CompactSpacingPreset = "Compact"; + + public string NormalizeSpacingPreset(string? value) + { + return string.Equals(value, CompactSpacingPreset, StringComparison.OrdinalIgnoreCase) + ? CompactSpacingPreset + : RelaxedSpacingPreset; + } + + public double ResolveGapRatio(string? preset) + { + return string.Equals(preset, CompactSpacingPreset, StringComparison.OrdinalIgnoreCase) ? 0.06 : 0.12; + } + + public double CalculateEdgeInset(double hostWidth, double hostHeight, int shortSideCells, int insetPercent) + { + if (hostWidth <= 1 || hostHeight <= 1) + { + return 0; + } + + var cells = Math.Max(1, shortSideCells); + var shortSidePx = Math.Max(1, Math.Min(hostWidth, hostHeight)); + var baseCell = shortSidePx / cells; + var insetRatio = Math.Clamp(insetPercent, 0, 30) / 100d; + return Math.Clamp(baseCell * insetRatio, 0, 80); + } + + public DesktopGridMetrics CalculateGridMetrics( + double hostWidth, + double hostHeight, + int shortSideCells, + double gapRatio, + double edgeInsetPx) + { + if (hostWidth <= 1 || hostHeight <= 1) + { + return default; + } + + var shortSide = Math.Max(1, shortSideCells); + var clampedGapRatio = Math.Max(0, gapRatio); + var inset = Math.Max(0, edgeInsetPx); + var availableWidth = Math.Max(1, hostWidth - inset * 2); + var availableHeight = Math.Max(1, hostHeight - inset * 2); + + if (hostWidth >= hostHeight) + { + var rowCount = shortSide; + var denominator = rowCount + Math.Max(0, rowCount - 1) * clampedGapRatio; + if (denominator <= 0) + { + return default; + } + + var cellSize = availableHeight / denominator; + var gapPx = cellSize * clampedGapRatio; + var pitch = cellSize + gapPx; + if (pitch <= 0) + { + return default; + } + + var columnCount = Math.Max(1, (int)Math.Floor((availableWidth + gapPx) / pitch)); + var gridWidth = columnCount * cellSize + Math.Max(0, columnCount - 1) * gapPx; + var gridHeight = rowCount * cellSize + Math.Max(0, rowCount - 1) * gapPx; + return new DesktopGridMetrics(columnCount, rowCount, cellSize, gapPx, inset, gridWidth, gridHeight); + } + + var columnCountPortrait = shortSide; + var denominatorPortrait = columnCountPortrait + Math.Max(0, columnCountPortrait - 1) * clampedGapRatio; + if (denominatorPortrait <= 0) + { + return default; + } + + var cellSizePortrait = availableWidth / denominatorPortrait; + var gapPxPortrait = cellSizePortrait * clampedGapRatio; + var pitchPortrait = cellSizePortrait + gapPxPortrait; + if (pitchPortrait <= 0) + { + return default; + } + + var rowCountPortrait = Math.Max(1, (int)Math.Floor((availableHeight + gapPxPortrait) / pitchPortrait)); + var gridWidthPortrait = columnCountPortrait * cellSizePortrait + Math.Max(0, columnCountPortrait - 1) * gapPxPortrait; + var gridHeightPortrait = rowCountPortrait * cellSizePortrait + Math.Max(0, rowCountPortrait - 1) * gapPxPortrait; + return new DesktopGridMetrics( + columnCountPortrait, + rowCountPortrait, + cellSizePortrait, + gapPxPortrait, + inset, + gridWidthPortrait, + gridHeightPortrait); + } +} diff --git a/LanMountainDesktop/Services/IndependentSettingsModuleService.cs b/LanMountainDesktop/Services/IndependentSettingsModuleService.cs deleted file mode 100644 index 99927a9..0000000 --- a/LanMountainDesktop/Services/IndependentSettingsModuleService.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using Avalonia.Threading; -using LanMountainDesktop.Views; - -namespace LanMountainDesktop.Services; - -internal sealed class IndependentSettingsModuleService -{ - private SettingsWindow? _window; - - public void ShowOrActivate(string source, string? pageTag = null) - { - AppLogger.Info("IndependentSettingsModule", $"OpenRequested; Source='{source}'; PageTag='{pageTag ?? ""}'."); - - void ShowCore() - { - try - { - if (_window is not { } window) - { - AppLogger.Info("IndependentSettingsModule", $"WindowConstructionStarted; Source='{source}'."); - window = new SettingsWindow(); - AppLogger.Info("IndependentSettingsModule", $"WindowConstructionCompleted; Source='{source}'."); - window.Closed += (_, _) => - { - if (ReferenceEquals(_window, window)) - { - _window = null; - } - - AppLogger.Info("IndependentSettingsModule", "WindowClosed."); - }; - _window = window; - } - - window.Open(pageTag); - AppLogger.Info( - "IndependentSettingsModule", - $"WindowActivated; Source='{source}'; ReusedExisting={ReferenceEquals(_window, window)}; WasVisible={window.IsVisible}; PageTag='{pageTag ?? ""}'."); - } - catch (Exception ex) - { - AppLogger.Warn("IndependentSettingsModule", $"Failed to open independent settings module window. Source='{source}'.", ex); - } - } - - if (Dispatcher.UIThread.CheckAccess()) - { - ShowCore(); - return; - } - - Dispatcher.UIThread.Post(ShowCore, DispatcherPriority.Normal); - } - - public void CloseIfOpen() - { - void CloseCore() - { - if (_window is null) - { - return; - } - - try - { - _window.PrepareForForceClose(); - _window.Close(); - } - catch (Exception ex) - { - AppLogger.Warn("IndependentSettingsModule", "Failed to close independent settings module window during shutdown.", ex); - } - finally - { - _window = null; - } - } - - if (Dispatcher.UIThread.CheckAccess()) - { - CloseCore(); - return; - } - - Dispatcher.UIThread.Post(CloseCore, DispatcherPriority.Send); - } -} diff --git a/LanMountainDesktop/Services/Settings/SettingsCatalogService.cs b/LanMountainDesktop/Services/Settings/SettingsCatalogService.cs new file mode 100644 index 0000000..9cf3a12 --- /dev/null +++ b/LanMountainDesktop/Services/Settings/SettingsCatalogService.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LanMountainDesktop.PluginSdk; + +namespace LanMountainDesktop.Services.Settings; + +internal sealed class SettingsCatalogService : ISettingsCatalog +{ + private readonly List _sections = []; + private readonly object _gate = new(); + + public SettingsCatalogService() + { + // Built-in host sections for the next settings UI. + _sections.AddRange( + [ + new SettingsSectionDefinition("general", SettingsCategories.General, SettingsScope.App, "settings.general.title", iconKey: "Settings", sortOrder: 0), + new SettingsSectionDefinition("appearance", SettingsCategories.Appearance, SettingsScope.App, "settings.appearance.title", iconKey: "DesignIdeas", sortOrder: 10), + new SettingsSectionDefinition("components", SettingsCategories.Components, SettingsScope.ComponentInstance, "settings.components.title", iconKey: "GridDots", sortOrder: 20), + new SettingsSectionDefinition("plugins", SettingsCategories.Plugins, SettingsScope.Plugin, "settings.plugins.title", iconKey: "PuzzlePiece", sortOrder: 30), + new SettingsSectionDefinition("plugin-market", SettingsCategories.PluginMarket, SettingsScope.Plugin, "settings.plugin_market.title", iconKey: "Shop", sortOrder: 40), + new SettingsSectionDefinition("update", SettingsCategories.Update, SettingsScope.App, "settings.update.title", iconKey: "ArrowSync", sortOrder: 50), + new SettingsSectionDefinition("about", SettingsCategories.About, SettingsScope.App, "settings.about.title", iconKey: "Info", sortOrder: 60), + new SettingsSectionDefinition("advanced", SettingsCategories.Advanced, SettingsScope.App, "settings.advanced.title", iconKey: "DeveloperBoard", sortOrder: 70) + ]); + } + + public IReadOnlyList GetSections() + { + lock (_gate) + { + return _sections + .OrderBy(section => section.SortOrder) + .ThenBy(section => section.Id, StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + } + + public IReadOnlyList GetSections(SettingsScope scope) + { + lock (_gate) + { + return _sections + .Where(section => section.Scope == scope) + .OrderBy(section => section.SortOrder) + .ThenBy(section => section.Id, StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + } + + public void RegisterPluginSections(string pluginId, IReadOnlyList sections) + { + ArgumentException.ThrowIfNullOrWhiteSpace(pluginId); + var normalizedPluginId = pluginId.Trim(); + + lock (_gate) + { + _sections.RemoveAll(section => + section.Scope == SettingsScope.Plugin && + string.Equals(section.SubjectId, normalizedPluginId, StringComparison.OrdinalIgnoreCase)); + + foreach (var registration in sections) + { + var definition = new SettingsSectionDefinition( + id: $"{normalizedPluginId}:{registration.Id}", + category: SettingsCategories.External, + scope: SettingsScope.Plugin, + titleLocalizationKey: registration.TitleLocalizationKey, + descriptionLocalizationKey: registration.DescriptionLocalizationKey, + iconKey: registration.IconKey, + sortOrder: registration.SortOrder, + subjectId: normalizedPluginId, + options: registration.Options); + _sections.Add(definition); + } + } + } + + public void RemovePluginSections(string pluginId) + { + if (string.IsNullOrWhiteSpace(pluginId)) + { + return; + } + + lock (_gate) + { + _sections.RemoveAll(section => + section.Scope == SettingsScope.Plugin && + string.Equals(section.SubjectId, pluginId, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs new file mode 100644 index 0000000..0b75104 --- /dev/null +++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using LanMountainDesktop.Models; +using LanMountainDesktop.PluginSdk; + +namespace LanMountainDesktop.Services.Settings; + +public enum WallpaperMediaType +{ + None, + Image, + Video +} + +public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent); +public sealed record WallpaperSettingsState(string? WallpaperPath, string Placement); +public sealed record ThemeAppearanceSettingsState(bool IsNightMode, string? ThemeColor); +public sealed record StatusBarSettingsState( + IReadOnlyList TopStatusComponentIds, + IReadOnlyList PinnedTaskbarActions, + bool EnableDynamicTaskbarActions, + string TaskbarLayoutMode, + string ClockDisplayFormat, + string SpacingMode, + int CustomSpacingPercent); +public sealed record WeatherSettingsState( + string LocationMode, + string LocationKey, + string LocationName, + double Latitude, + double Longitude, + bool AutoRefreshLocation, + string ExcludedAlerts, + string IconPackId, + bool NoTlsRequests, + string LocationQuery); +public sealed record RegionSettingsState(string LanguageCode, string? TimeZoneId); +public sealed record UpdateSettingsState(bool AutoCheckUpdates, bool IncludePrereleaseUpdates, string UpdateChannel); +public sealed record PluginManagementSettingsState(IReadOnlyList DisabledPluginIds); +public sealed record PluginMarketPluginInfo( + string Id, + string Name, + string Description, + string Author, + string Version, + string ApiVersion, + string MinHostVersion, + string DownloadUrl, + string ReleaseTag, + string ReleaseAssetName, + string IconUrl, + string ReadmeUrl, + string HomepageUrl, + string RepositoryUrl, + IReadOnlyList Tags, + DateTimeOffset PublishedAt, + DateTimeOffset UpdatedAt); +public sealed record PluginMarketIndexResult( + bool Success, + IReadOnlyList Plugins, + string? Source, + string? SourceLocation, + string? WarningMessage, + string? ErrorMessage); +public sealed record PluginMarketInstallResult( + bool Success, + string? PluginId, + string? PluginName, + string? ErrorMessage); + +public interface IGridSettingsService +{ + GridSettingsState Get(); + void Save(GridSettingsState state); +} + +public interface IWallpaperSettingsService +{ + WallpaperSettingsState Get(); + void Save(WallpaperSettingsState state); +} + +public interface IWallpaperMediaService +{ + WallpaperMediaType DetectMediaType(string? path); + Task ImportAssetAsync(string sourcePath, CancellationToken cancellationToken = default); +} + +public interface IThemeAppearanceService +{ + ThemeAppearanceSettingsState Get(); + void Save(ThemeAppearanceSettingsState state); + MonetPalette BuildPalette(bool nightMode, string? wallpaperPath); +} + +public interface IStatusBarSettingsService +{ + StatusBarSettingsState Get(); + void Save(StatusBarSettingsState state); +} + +public interface IWeatherProvider +{ + Task>> SearchLocationsAsync( + string keyword, + string? locale = null, + CancellationToken cancellationToken = default); + + Task> GetWeatherAsync( + WeatherQuery query, + CancellationToken cancellationToken = default); +} + +public interface IWeatherSettingsService +{ + WeatherSettingsState Get(); + void Save(WeatherSettingsState state); +} + +public interface IRegionSettingsService +{ + RegionSettingsState Get(); + void Save(RegionSettingsState state); +} + +public interface IUpdateSettingsService +{ + UpdateSettingsState Get(); + void Save(UpdateSettingsState state); + Task CheckForUpdatesAsync(Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default); + Task DownloadAssetAsync( + GitHubReleaseAsset asset, + string destinationFilePath, + IProgress? progress = null, + CancellationToken cancellationToken = default); +} + +public interface ILauncherCatalogService +{ + StartMenuFolderNode LoadCatalog(); +} + +public interface ILauncherPolicyService +{ + LauncherSettingsSnapshot Get(); + void Save(LauncherSettingsSnapshot snapshot); +} + +public interface IPluginManagementSettingsService +{ + PluginManagementSettingsState Get(); + void Save(PluginManagementSettingsState state); + IReadOnlyList GetInstalledPlugins(); + bool SetPluginEnabled(string pluginId, bool isEnabled); + bool DeleteInstalledPlugin(string pluginId); +} + +public interface IPluginMarketSettingsService +{ + Task LoadIndexAsync(CancellationToken cancellationToken = default); + Task InstallAsync(string pluginId, CancellationToken cancellationToken = default); +} + +public interface IApplicationInfoService +{ + string GetAppVersionText(); + AppRenderBackendInfo GetRenderBackendInfo(); +} + +public interface ISettingsFacadeService +{ + ISettingsService Settings { get; } + ISettingsCatalog Catalog { get; } + IGridSettingsService Grid { get; } + IWallpaperSettingsService Wallpaper { get; } + IWallpaperMediaService WallpaperMedia { get; } + IThemeAppearanceService Theme { get; } + IStatusBarSettingsService StatusBar { get; } + IWeatherSettingsService Weather { get; } + IRegionSettingsService Region { get; } + IUpdateSettingsService Update { get; } + ILauncherCatalogService LauncherCatalog { get; } + ILauncherPolicyService LauncherPolicy { get; } + IPluginManagementSettingsService PluginManagement { get; } + IPluginMarketSettingsService PluginMarket { get; } + IApplicationInfoService ApplicationInfo { get; } +} diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs new file mode 100644 index 0000000..40f942d --- /dev/null +++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs @@ -0,0 +1,627 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Media.Imaging; +using LanMountainDesktop.Models; +using LanMountainDesktop.PluginSdk; +using LanMountainDesktop.Services.PluginMarket; + +namespace LanMountainDesktop.Services.Settings; + +internal sealed class GridSettingsService : IGridSettingsService +{ + private readonly AppSettingsService _appSettingsService = new(); + + public GridSettingsState Get() + { + var snapshot = _appSettingsService.Load(); + return new GridSettingsState( + snapshot.GridShortSideCells, + snapshot.GridSpacingPreset, + snapshot.DesktopEdgeInsetPercent); + } + + public void Save(GridSettingsState state) + { + var snapshot = _appSettingsService.Load(); + snapshot.GridShortSideCells = state.ShortSideCells; + snapshot.GridSpacingPreset = state.SpacingPreset; + snapshot.DesktopEdgeInsetPercent = state.EdgeInsetPercent; + _appSettingsService.Save(snapshot); + } +} + +internal sealed class WallpaperSettingsService : IWallpaperSettingsService +{ + private readonly AppSettingsService _appSettingsService = new(); + + public WallpaperSettingsState Get() + { + var snapshot = _appSettingsService.Load(); + return new WallpaperSettingsState(snapshot.WallpaperPath, snapshot.WallpaperPlacement); + } + + public void Save(WallpaperSettingsState state) + { + var snapshot = _appSettingsService.Load(); + snapshot.WallpaperPath = state.WallpaperPath; + snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement) + ? "Fill" + : state.Placement.Trim(); + _appSettingsService.Save(snapshot); + } +} + +internal sealed class WallpaperMediaService : IWallpaperMediaService +{ + private static readonly HashSet ImageExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp" + }; + + private static readonly HashSet VideoExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v" + }; + + private readonly string _wallpapersDirectory; + + public WallpaperMediaService() + { + var appDataRoot = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "LanMountainDesktop"); + _wallpapersDirectory = Path.Combine(appDataRoot, "Wallpapers"); + } + + public WallpaperMediaType DetectMediaType(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return WallpaperMediaType.None; + } + + var extension = Path.GetExtension(path.Trim()); + if (string.IsNullOrWhiteSpace(extension)) + { + return WallpaperMediaType.None; + } + + if (ImageExtensions.Contains(extension)) + { + return WallpaperMediaType.Image; + } + + if (VideoExtensions.Contains(extension)) + { + return WallpaperMediaType.Video; + } + + return WallpaperMediaType.None; + } + + public async Task ImportAssetAsync(string sourcePath, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(sourcePath)) + { + return null; + } + + var fullSourcePath = Path.GetFullPath(sourcePath); + if (!File.Exists(fullSourcePath)) + { + return null; + } + + if (DetectMediaType(fullSourcePath) == WallpaperMediaType.None) + { + return null; + } + + Directory.CreateDirectory(_wallpapersDirectory); + + var extension = Path.GetExtension(fullSourcePath); + var baseName = Path.GetFileNameWithoutExtension(fullSourcePath); + var normalizedBaseName = string.IsNullOrWhiteSpace(baseName) + ? "wallpaper" + : string.Concat(baseName.Select(ch => Path.GetInvalidFileNameChars().Contains(ch) ? '_' : ch)); + + var destinationPath = Path.Combine(_wallpapersDirectory, $"{normalizedBaseName}{extension}"); + if (string.Equals(fullSourcePath, destinationPath, StringComparison.OrdinalIgnoreCase)) + { + return destinationPath; + } + + var suffix = 1; + while (File.Exists(destinationPath)) + { + destinationPath = Path.Combine(_wallpapersDirectory, $"{normalizedBaseName}_{suffix}{extension}"); + suffix++; + } + + await using var source = File.OpenRead(fullSourcePath); + await using var destination = File.Create(destinationPath); + await source.CopyToAsync(destination, cancellationToken); + return destinationPath; + } +} + +internal sealed class ThemeAppearanceService : IThemeAppearanceService +{ + private readonly AppSettingsService _appSettingsService = new(); + private readonly MonetColorService _monetColorService = new(); + private readonly WallpaperMediaService _wallpaperMediaService = new(); + + public ThemeAppearanceSettingsState Get() + { + var snapshot = _appSettingsService.Load(); + return new ThemeAppearanceSettingsState( + snapshot.IsNightMode ?? false, + snapshot.ThemeColor); + } + + public void Save(ThemeAppearanceSettingsState state) + { + var snapshot = _appSettingsService.Load(); + snapshot.IsNightMode = state.IsNightMode; + snapshot.ThemeColor = state.ThemeColor; + _appSettingsService.Save(snapshot); + } + + public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath) + { + Bitmap? bitmap = null; + + try + { + if (_wallpaperMediaService.DetectMediaType(wallpaperPath) == WallpaperMediaType.Image && + !string.IsNullOrWhiteSpace(wallpaperPath) && + File.Exists(wallpaperPath)) + { + bitmap = new Bitmap(wallpaperPath); + } + } + catch (Exception ex) + { + AppLogger.Warn( + "Settings.Theme", + $"Failed to load wallpaper bitmap for palette generation. Path='{wallpaperPath}'.", + ex); + } + + try + { + return _monetColorService.BuildPalette(bitmap, nightMode); + } + finally + { + bitmap?.Dispose(); + } + } +} + +internal sealed class StatusBarSettingsService : IStatusBarSettingsService +{ + private readonly AppSettingsService _appSettingsService = new(); + + public StatusBarSettingsState Get() + { + var snapshot = _appSettingsService.Load(); + return new StatusBarSettingsState( + snapshot.TopStatusComponentIds?.ToArray() ?? [], + snapshot.PinnedTaskbarActions?.ToArray() ?? [], + snapshot.EnableDynamicTaskbarActions, + snapshot.TaskbarLayoutMode, + snapshot.ClockDisplayFormat, + snapshot.StatusBarSpacingMode, + snapshot.StatusBarCustomSpacingPercent); + } + + public void Save(StatusBarSettingsState state) + { + var snapshot = _appSettingsService.Load(); + snapshot.TopStatusComponentIds = state.TopStatusComponentIds?.ToList() ?? []; + snapshot.PinnedTaskbarActions = state.PinnedTaskbarActions?.ToList() ?? []; + snapshot.EnableDynamicTaskbarActions = state.EnableDynamicTaskbarActions; + snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode; + snapshot.ClockDisplayFormat = state.ClockDisplayFormat; + snapshot.StatusBarSpacingMode = state.SpacingMode; + snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent; + _appSettingsService.Save(snapshot); + } +} + +internal sealed class WeatherProviderAdapter : IWeatherProvider +{ + private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService(); + + public Task>> SearchLocationsAsync( + string keyword, + string? locale = null, + CancellationToken cancellationToken = default) + { + return _weatherDataService.SearchLocationsAsync(keyword, locale, cancellationToken); + } + + public Task> GetWeatherAsync( + WeatherQuery query, + CancellationToken cancellationToken = default) + { + return _weatherDataService.GetWeatherAsync(query, cancellationToken); + } +} + +internal sealed class WeatherSettingsService : IWeatherSettingsService +{ + private readonly AppSettingsService _appSettingsService = new(); + + public WeatherSettingsState Get() + { + var snapshot = _appSettingsService.Load(); + return new WeatherSettingsState( + snapshot.WeatherLocationMode, + snapshot.WeatherLocationKey, + snapshot.WeatherLocationName, + snapshot.WeatherLatitude, + snapshot.WeatherLongitude, + snapshot.WeatherAutoRefreshLocation, + snapshot.WeatherExcludedAlerts, + snapshot.WeatherIconPackId, + snapshot.WeatherNoTlsRequests, + snapshot.WeatherLocationQuery); + } + + public void Save(WeatherSettingsState state) + { + var snapshot = _appSettingsService.Load(); + snapshot.WeatherLocationMode = state.LocationMode; + snapshot.WeatherLocationKey = state.LocationKey; + snapshot.WeatherLocationName = state.LocationName; + snapshot.WeatherLatitude = state.Latitude; + snapshot.WeatherLongitude = state.Longitude; + snapshot.WeatherAutoRefreshLocation = state.AutoRefreshLocation; + snapshot.WeatherExcludedAlerts = state.ExcludedAlerts; + snapshot.WeatherIconPackId = state.IconPackId; + snapshot.WeatherNoTlsRequests = state.NoTlsRequests; + snapshot.WeatherLocationQuery = state.LocationQuery; + _appSettingsService.Save(snapshot); + } +} + +internal sealed class RegionSettingsService : IRegionSettingsService +{ + private readonly AppSettingsService _appSettingsService = new(); + + public RegionSettingsState Get() + { + var snapshot = _appSettingsService.Load(); + return new RegionSettingsState(snapshot.LanguageCode, snapshot.TimeZoneId); + } + + public void Save(RegionSettingsState state) + { + var snapshot = _appSettingsService.Load(); + snapshot.LanguageCode = string.IsNullOrWhiteSpace(state.LanguageCode) + ? "zh-CN" + : state.LanguageCode.Trim(); + snapshot.TimeZoneId = string.IsNullOrWhiteSpace(state.TimeZoneId) + ? null + : state.TimeZoneId.Trim(); + _appSettingsService.Save(snapshot); + } +} + +internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable +{ + private readonly AppSettingsService _appSettingsService = new(); + private readonly GitHubReleaseUpdateService _releaseUpdateService = new("wwiinnddyy", "LanMountainDesktop"); + + public UpdateSettingsState Get() + { + var snapshot = _appSettingsService.Load(); + return new UpdateSettingsState( + snapshot.AutoCheckUpdates, + snapshot.IncludePrereleaseUpdates, + snapshot.UpdateChannel); + } + + public void Save(UpdateSettingsState state) + { + var snapshot = _appSettingsService.Load(); + snapshot.AutoCheckUpdates = state.AutoCheckUpdates; + snapshot.IncludePrereleaseUpdates = state.IncludePrereleaseUpdates; + snapshot.UpdateChannel = state.UpdateChannel; + _appSettingsService.Save(snapshot); + } + + public Task CheckForUpdatesAsync( + Version currentVersion, + bool includePrerelease, + CancellationToken cancellationToken = default) + { + return _releaseUpdateService.CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken); + } + + public Task DownloadAssetAsync( + GitHubReleaseAsset asset, + string destinationFilePath, + IProgress? progress = null, + CancellationToken cancellationToken = default) + { + return _releaseUpdateService.DownloadAssetAsync(asset, destinationFilePath, progress, cancellationToken); + } + + public void Dispose() + { + _releaseUpdateService.Dispose(); + } +} + +internal sealed class LauncherCatalogService : ILauncherCatalogService +{ + private readonly WindowsStartMenuService _startMenuService = new(); + + public StartMenuFolderNode LoadCatalog() + { + return _startMenuService.Load(); + } +} + +internal sealed class LauncherPolicyService : ILauncherPolicyService +{ + private readonly LauncherSettingsService _launcherSettingsService = new(); + + public LauncherSettingsSnapshot Get() + { + return _launcherSettingsService.Load(); + } + + public void Save(LauncherSettingsSnapshot snapshot) + { + _launcherSettingsService.Save(snapshot ?? new LauncherSettingsSnapshot()); + } +} + +internal sealed class PluginManagementSettingsService : IPluginManagementSettingsService +{ + private readonly AppSettingsService _appSettingsService = new(); + private readonly PluginRuntimeService? _pluginRuntimeService; + + public PluginManagementSettingsService(PluginRuntimeService? pluginRuntimeService) + { + _pluginRuntimeService = pluginRuntimeService; + } + + public PluginManagementSettingsState Get() + { + var snapshot = _appSettingsService.Load(); + return new PluginManagementSettingsState(snapshot.DisabledPluginIds?.ToArray() ?? []); + } + + public void Save(PluginManagementSettingsState state) + { + var snapshot = _appSettingsService.Load(); + snapshot.DisabledPluginIds = state.DisabledPluginIds?.ToList() ?? []; + _appSettingsService.Save(snapshot); + } + + public IReadOnlyList GetInstalledPlugins() + { + return _pluginRuntimeService?.GetInstalledPluginsSnapshot() ?? []; + } + + public bool SetPluginEnabled(string pluginId, bool isEnabled) + { + return _pluginRuntimeService?.SetPluginEnabled(pluginId, isEnabled) ?? false; + } + + public bool DeleteInstalledPlugin(string pluginId) + { + return _pluginRuntimeService?.DeleteInstalledPlugin(pluginId) ?? false; + } +} + +internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService, IDisposable +{ + private readonly PluginRuntimeService? _pluginRuntimeService; + private readonly AirAppMarketIndexService _indexService; + private readonly AirAppMarketInstallService? _installService; + private readonly Dictionary _cachedPlugins = new(StringComparer.OrdinalIgnoreCase); + + public PluginMarketSettingsService(PluginRuntimeService? pluginRuntimeService) + { + _pluginRuntimeService = pluginRuntimeService; + + var dataRoot = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "LanMountainDesktop", + "PluginMarket"); + var cacheService = new AirAppMarketCacheService(dataRoot); + _indexService = new AirAppMarketIndexService(cacheService); + if (_pluginRuntimeService is not null) + { + _installService = new AirAppMarketInstallService(_pluginRuntimeService, dataRoot); + } + } + + public async Task LoadIndexAsync(CancellationToken cancellationToken = default) + { + var result = await _indexService.LoadAsync(cancellationToken); + if (!result.Success || result.Document is null) + { + return new PluginMarketIndexResult( + false, + [], + result.Source?.ToString(), + result.SourceLocation, + result.WarningMessage, + result.ErrorMessage); + } + + _cachedPlugins.Clear(); + var plugins = result.Document.Plugins + .Select(entry => + { + _cachedPlugins[entry.Id] = entry; + return new PluginMarketPluginInfo( + entry.Id, + entry.Name, + entry.Description, + entry.Author, + entry.Version, + entry.ApiVersion, + entry.MinHostVersion, + entry.DownloadUrl, + entry.ReleaseTag, + entry.ReleaseAssetName, + entry.IconUrl, + entry.ReadmeUrl, + entry.HomepageUrl, + entry.RepositoryUrl, + entry.Tags, + entry.PublishedAt, + entry.UpdatedAt); + }) + .ToArray(); + + return new PluginMarketIndexResult( + true, + plugins, + result.Source?.ToString(), + result.SourceLocation, + result.WarningMessage, + null); + } + + public async Task InstallAsync( + string pluginId, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(pluginId)) + { + return new PluginMarketInstallResult(false, null, null, "Plugin id is required."); + } + + if (_installService is null || _pluginRuntimeService is null) + { + return new PluginMarketInstallResult( + false, + pluginId, + null, + "Plugin runtime is unavailable."); + } + + if (!_cachedPlugins.TryGetValue(pluginId, out var entry)) + { + var load = await LoadIndexAsync(cancellationToken); + if (!load.Success) + { + return new PluginMarketInstallResult(false, pluginId, null, load.ErrorMessage); + } + + if (!_cachedPlugins.TryGetValue(pluginId, out entry)) + { + return new PluginMarketInstallResult(false, pluginId, null, "Plugin was not found in market index."); + } + } + + var result = await _installService.InstallAsync(entry, cancellationToken); + if (!result.Success) + { + return new PluginMarketInstallResult(false, entry.Id, entry.Name, result.ErrorMessage); + } + + return new PluginMarketInstallResult(true, result.Manifest?.Id ?? entry.Id, result.Manifest?.Name ?? entry.Name, null); + } + + public void Dispose() + { + _indexService.Dispose(); + _installService?.Dispose(); + } +} + +internal sealed class ApplicationInfoService : IApplicationInfoService +{ + public string GetAppVersionText() + { + var version = typeof(App).Assembly.GetName().Version; + return version is null + ? "0.0.0" + : new Version( + Math.Max(0, version.Major), + Math.Max(0, version.Minor), + Math.Max(0, version.Build)).ToString(3); + } + + public AppRenderBackendInfo GetRenderBackendInfo() + { + return AppRenderBackendDiagnostics.Detect(); + } +} + +internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposable +{ + private readonly UpdateSettingsService _updateSettingsService; + private readonly PluginMarketSettingsService _pluginMarketSettingsService; + + public SettingsFacadeService(PluginRuntimeService? pluginRuntimeService = null) + { + Settings = new SettingsService(); + Catalog = new SettingsCatalogService(); + Grid = new GridSettingsService(); + Wallpaper = new WallpaperSettingsService(); + WallpaperMedia = new WallpaperMediaService(); + Theme = new ThemeAppearanceService(); + StatusBar = new StatusBarSettingsService(); + Weather = new WeatherSettingsService(); + Region = new RegionSettingsService(); + _updateSettingsService = new UpdateSettingsService(); + Update = _updateSettingsService; + LauncherCatalog = new LauncherCatalogService(); + LauncherPolicy = new LauncherPolicyService(); + PluginManagement = new PluginManagementSettingsService(pluginRuntimeService); + _pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService); + PluginMarket = _pluginMarketSettingsService; + ApplicationInfo = new ApplicationInfoService(); + } + + public ISettingsService Settings { get; } + + public ISettingsCatalog Catalog { get; } + + public IGridSettingsService Grid { get; } + + public IWallpaperSettingsService Wallpaper { get; } + + public IWallpaperMediaService WallpaperMedia { get; } + + public IThemeAppearanceService Theme { get; } + + public IStatusBarSettingsService StatusBar { get; } + + public IWeatherSettingsService Weather { get; } + + public IRegionSettingsService Region { get; } + + public IUpdateSettingsService Update { get; } + + public ILauncherCatalogService LauncherCatalog { get; } + + public ILauncherPolicyService LauncherPolicy { get; } + + public IPluginManagementSettingsService PluginManagement { get; } + + public IPluginMarketSettingsService PluginMarket { get; } + + public IApplicationInfoService ApplicationInfo { get; } + + public void Dispose() + { + _updateSettingsService.Dispose(); + _pluginMarketSettingsService.Dispose(); + } +} diff --git a/LanMountainDesktop/Services/Settings/SettingsService.cs b/LanMountainDesktop/Services/Settings/SettingsService.cs new file mode 100644 index 0000000..fc5487e --- /dev/null +++ b/LanMountainDesktop/Services/Settings/SettingsService.cs @@ -0,0 +1,423 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using LanMountainDesktop.Models; +using LanMountainDesktop.PluginSdk; + +namespace LanMountainDesktop.Services.Settings; + +internal sealed class SettingsService : ISettingsService +{ + private static readonly JsonSerializerOptions SerializerOptions = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + private readonly AppSettingsService _appSettingsService = new(); + private readonly LauncherSettingsService _launcherSettingsService = new(); + private readonly ComponentSettingsService _componentSettingsService = new(); + private readonly string _pluginSettingsPath; + private readonly object _pluginSettingsGate = new(); + + public SettingsService() + { + var root = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "LanMountainDesktop"); + _pluginSettingsPath = Path.Combine(root, "plugin-settings.json"); + } + + public event EventHandler? Changed; + + public T LoadSnapshot(SettingsScope scope, string? subjectId = null, string? placementId = null) where T : new() + { + return scope switch + { + SettingsScope.App => ConvertSnapshot(_appSettingsService.Load()), + SettingsScope.Launcher => ConvertSnapshot(_launcherSettingsService.Load()), + SettingsScope.ComponentInstance => LoadComponentSnapshot(subjectId, placementId), + SettingsScope.Plugin => LoadSection(scope, EnsureKey(subjectId), sectionId: "__snapshot__", placementId), + _ => new T() + }; + } + + public void SaveSnapshot( + SettingsScope scope, + T snapshot, + string? subjectId = null, + string? placementId = null, + string? sectionId = null, + IReadOnlyCollection? changedKeys = null) + { + switch (scope) + { + case SettingsScope.App: + _appSettingsService.Save(ConvertSnapshot(snapshot)); + break; + case SettingsScope.Launcher: + _launcherSettingsService.Save(ConvertSnapshot(snapshot)); + break; + case SettingsScope.ComponentInstance: + SaveComponentSnapshot(subjectId, placementId, snapshot); + break; + case SettingsScope.Plugin: + SaveSection(scope, EnsureKey(subjectId), "__snapshot__", snapshot, placementId, changedKeys); + break; + } + + OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys)); + } + + public T LoadSection( + SettingsScope scope, + string subjectId, + string sectionId, + string? placementId = null) where T : new() + { + if (scope == SettingsScope.ComponentInstance) + { + return _componentSettingsService.LoadPluginSettings(EnsureKey(subjectId), placementId); + } + + if (scope != SettingsScope.Plugin) + { + return new T(); + } + + lock (_pluginSettingsGate) + { + var document = LoadPluginDocumentLocked(); + if (!document.Sections.TryGetValue(EnsureKey(subjectId), out var pluginSections) || + !pluginSections.TryGetValue(EnsureKey(sectionId), out var payload)) + { + return new T(); + } + + return JsonSerializer.Deserialize(payload.GetRawText(), SerializerOptions) ?? new T(); + } + } + + public void SaveSection( + SettingsScope scope, + string subjectId, + string sectionId, + T section, + string? placementId = null, + IReadOnlyCollection? changedKeys = null) + { + if (scope == SettingsScope.ComponentInstance) + { + _componentSettingsService.SavePluginSettings(EnsureKey(subjectId), placementId, section); + OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys)); + return; + } + + if (scope != SettingsScope.Plugin) + { + return; + } + + lock (_pluginSettingsGate) + { + var document = LoadPluginDocumentLocked(); + var pluginId = EnsureKey(subjectId); + if (!document.Sections.TryGetValue(pluginId, out var pluginSections)) + { + pluginSections = new Dictionary(StringComparer.OrdinalIgnoreCase); + document.Sections[pluginId] = pluginSections; + } + + pluginSections[EnsureKey(sectionId)] = JsonSerializer.SerializeToElement(section, SerializerOptions).Clone(); + PersistPluginDocumentLocked(document); + } + + OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys)); + } + + public void DeleteSection(SettingsScope scope, string subjectId, string sectionId, string? placementId = null) + { + if (scope == SettingsScope.ComponentInstance) + { + _componentSettingsService.DeletePluginSettings(EnsureKey(subjectId), placementId); + OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId)); + return; + } + + if (scope != SettingsScope.Plugin) + { + return; + } + + lock (_pluginSettingsGate) + { + var document = LoadPluginDocumentLocked(); + var pluginId = EnsureKey(subjectId); + if (document.Sections.TryGetValue(pluginId, out var sections) && + sections.Remove(EnsureKey(sectionId))) + { + if (sections.Count == 0) + { + document.Sections.Remove(pluginId); + } + + PersistPluginDocumentLocked(document); + } + } + + OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId)); + } + + public T? GetValue( + SettingsScope scope, + string key, + string? subjectId = null, + string? placementId = null, + string? sectionId = null) + { + var snapshot = scope switch + { + SettingsScope.App => JsonSerializer.SerializeToElement(_appSettingsService.Load(), SerializerOptions), + SettingsScope.Launcher => JsonSerializer.SerializeToElement(_launcherSettingsService.Load(), SerializerOptions), + SettingsScope.ComponentInstance => JsonSerializer.SerializeToElement( + _componentSettingsService.LoadForComponent(EnsureKey(subjectId), placementId), + SerializerOptions), + SettingsScope.Plugin => JsonSerializer.SerializeToElement( + LoadSection>(SettingsScope.Plugin, EnsureKey(subjectId), sectionId ?? "__root__", placementId), + SerializerOptions), + _ => default + }; + + if (snapshot.ValueKind != JsonValueKind.Object) + { + return default; + } + + foreach (var property in snapshot.EnumerateObject()) + { + if (!string.Equals(property.Name, key, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + try + { + return property.Value.Deserialize(SerializerOptions); + } + catch + { + return default; + } + } + + return default; + } + + public void SetValue( + SettingsScope scope, + string key, + T value, + string? subjectId = null, + string? placementId = null, + string? sectionId = null, + IReadOnlyCollection? changedKeys = null) + { + if (scope == SettingsScope.Plugin) + { + var dict = LoadSection>( + SettingsScope.Plugin, + EnsureKey(subjectId), + sectionId ?? "__root__", + placementId); + dict[key] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone(); + SaveSection(SettingsScope.Plugin, EnsureKey(subjectId), sectionId ?? "__root__", dict, placementId, changedKeys ?? [key]); + return; + } + + if (scope == SettingsScope.ComponentInstance) + { + var dict = _componentSettingsService.LoadPluginSettings>(EnsureKey(subjectId), placementId); + dict[key] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone(); + _componentSettingsService.SavePluginSettings(EnsureKey(subjectId), placementId, dict); + OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys ?? [key])); + return; + } + + if (scope == SettingsScope.App) + { + var snapshot = _appSettingsService.Load(); + var updated = UpdateObjectKey(snapshot, key, value); + _appSettingsService.Save(updated); + OnChanged(new SettingsChangedEvent(scope, null, null, sectionId, changedKeys ?? [key])); + return; + } + + if (scope == SettingsScope.Launcher) + { + var snapshot = _launcherSettingsService.Load(); + var updated = UpdateObjectKey(snapshot, key, value); + _launcherSettingsService.Save(updated); + OnChanged(new SettingsChangedEvent(scope, null, null, sectionId, changedKeys ?? [key])); + } + } + + public IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId) + { + return new ComponentSettingsAccessor(this, componentId, placementId); + } + + private T LoadComponentSnapshot(string? componentId, string? placementId) where T : new() + { + var snapshot = _componentSettingsService.LoadForComponent(EnsureKey(componentId), placementId); + return ConvertSnapshot(snapshot); + } + + private void SaveComponentSnapshot(string? componentId, string? placementId, T snapshot) + { + var converted = ConvertSnapshot(snapshot); + _componentSettingsService.SaveForComponent(EnsureKey(componentId), placementId, converted); + } + + private static TOut ConvertSnapshot(TIn source) where TOut : new() + { + if (source is null) + { + return new TOut(); + } + + if (source is TOut direct) + { + return direct; + } + + try + { + var json = JsonSerializer.Serialize(source, SerializerOptions); + return JsonSerializer.Deserialize(json, SerializerOptions) ?? new TOut(); + } + catch + { + return new TOut(); + } + } + + private static TSnapshot UpdateObjectKey(TSnapshot snapshot, string key, TValue value) + where TSnapshot : new() + { + var bag = JsonSerializer.Deserialize>( + JsonSerializer.Serialize(snapshot, SerializerOptions), + SerializerOptions) ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + + var actualKey = bag.Keys.FirstOrDefault(existing => string.Equals(existing, key, StringComparison.OrdinalIgnoreCase)) ?? key; + bag[actualKey] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone(); + + try + { + var json = JsonSerializer.Serialize(bag, SerializerOptions); + return JsonSerializer.Deserialize(json, SerializerOptions) ?? new TSnapshot(); + } + catch + { + return snapshot is null ? new TSnapshot() : snapshot; + } + } + + private PluginSettingsDocument LoadPluginDocumentLocked() + { + try + { + if (!File.Exists(_pluginSettingsPath)) + { + return new PluginSettingsDocument(); + } + + var json = File.ReadAllText(_pluginSettingsPath); + return JsonSerializer.Deserialize(json, SerializerOptions) ?? new PluginSettingsDocument(); + } + catch (Exception ex) + { + AppLogger.Warn("SettingsService", $"Failed to load plugin settings '{_pluginSettingsPath}'.", ex); + return new PluginSettingsDocument(); + } + } + + private void PersistPluginDocumentLocked(PluginSettingsDocument document) + { + try + { + var directory = Path.GetDirectoryName(_pluginSettingsPath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(_pluginSettingsPath, JsonSerializer.Serialize(document, SerializerOptions)); + } + catch (Exception ex) + { + AppLogger.Warn("SettingsService", $"Failed to persist plugin settings '{_pluginSettingsPath}'.", ex); + } + } + + private static string EnsureKey(string? value) + { + return string.IsNullOrWhiteSpace(value) ? "__default__" : value.Trim(); + } + + private void OnChanged(SettingsChangedEvent e) + { + try + { + Changed?.Invoke(this, e); + } + catch + { + // Never let a subscriber break settings persistence. + } + } + + private sealed class ComponentSettingsAccessor : IComponentSettingsAccessor + { + private readonly SettingsService _settingsService; + + public ComponentSettingsAccessor(SettingsService settingsService, string componentId, string? placementId) + { + _settingsService = settingsService; + ComponentId = componentId; + PlacementId = placementId; + } + + public string ComponentId { get; } + + public string? PlacementId { get; } + + public T LoadSnapshot() where T : new() + => _settingsService.LoadSnapshot(SettingsScope.ComponentInstance, ComponentId, PlacementId); + + public void SaveSnapshot(T snapshot, IReadOnlyCollection? changedKeys = null) + => _settingsService.SaveSnapshot(SettingsScope.ComponentInstance, snapshot, ComponentId, PlacementId, changedKeys: changedKeys); + + public T LoadSection(string sectionId) where T : new() + => _settingsService.LoadSection(SettingsScope.ComponentInstance, ComponentId, sectionId, PlacementId); + + public void SaveSection(string sectionId, T section, IReadOnlyCollection? changedKeys = null) + => _settingsService.SaveSection(SettingsScope.ComponentInstance, ComponentId, sectionId, section, PlacementId, changedKeys); + + public void DeleteSection(string sectionId) + => _settingsService.DeleteSection(SettingsScope.ComponentInstance, ComponentId, sectionId, PlacementId); + + public T? GetValue(string key) + => _settingsService.GetValue(SettingsScope.ComponentInstance, key, ComponentId, PlacementId); + + public void SetValue(string key, T value, IReadOnlyCollection? changedKeys = null) + => _settingsService.SetValue(SettingsScope.ComponentInstance, key, value, ComponentId, PlacementId, changedKeys: changedKeys); + } + + private sealed class PluginSettingsDocument + { + public Dictionary> Sections { get; set; } = new(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/LanMountainDesktop/Views/Components/AnalogClockWidgetSettingsWindow.axaml b/LanMountainDesktop/Views/Components/AnalogClockWidgetSettingsWindow.axaml deleted file mode 100644 index 84f2c15..0000000 --- a/LanMountainDesktop/Views/Components/AnalogClockWidgetSettingsWindow.axaml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/AnalogClockWidgetSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/AnalogClockWidgetSettingsWindow.axaml.cs deleted file mode 100644 index d9e73ac..0000000 --- a/LanMountainDesktop/Views/Components/AnalogClockWidgetSettingsWindow.axaml.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using LanMountainDesktop.ComponentSystem; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class AnalogClockWidgetSettingsWindow : UserControl, IComponentPlacementContextAware, IComponentSettingsStoreAware -{ - private static readonly IReadOnlyDictionary ZhTimeZoneNames = - new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["China Standard Time"] = "中国标准时间", - ["Asia/Shanghai"] = "中国标准时间", - ["GMT Standard Time"] = "格林威治标准时间", - ["Europe/London"] = "格林威治标准时间", - ["AUS Eastern Standard Time"] = "澳大利亚东部标准时间", - ["Australia/Sydney"] = "澳大利亚东部标准时间", - ["Eastern Standard Time"] = "美国东部标准时间", - ["America/New_York"] = "美国东部标准时间", - ["Tokyo Standard Time"] = "日本标准时间", - ["Asia/Tokyo"] = "日本标准时间", - ["UTC"] = "协调世界时", - ["Etc/UTC"] = "协调世界时" - }; - - private readonly AppSettingsService _appSettingsService = new(); - private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService(); - private readonly LocalizationService _localizationService = new(); - private readonly TimeZoneService _timeZoneService = new(); - private bool _suppressEvents; - private string _languageCode = "zh-CN"; - private string _componentId = BuiltInComponentIds.DesktopClock; - private string _placementId = string.Empty; - private IReadOnlyList _allTimeZones = Array.Empty(); - private string _selectedTimeZoneId = string.Empty; - private string _secondHandMode = ClockSecondHandMode.Tick; - - public event EventHandler? SettingsChanged; - - public AnalogClockWidgetSettingsWindow() - { - InitializeComponent(); - LoadState(); - ApplyLocalization(); - PopulateTimeZoneComboBox(); - } - - public void SetComponentPlacementContext(string componentId, string? placementId) - { - _componentId = string.IsNullOrWhiteSpace(componentId) - ? BuiltInComponentIds.DesktopClock - : componentId.Trim(); - _placementId = placementId?.Trim() ?? string.Empty; - LoadState(); - ApplyLocalization(); - PopulateTimeZoneComboBox(); - } - - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - LoadState(); - ApplyLocalization(); - PopulateTimeZoneComboBox(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - _selectedTimeZoneId = string.IsNullOrWhiteSpace(componentSnapshot.DesktopClockTimeZoneId) - ? "China Standard Time" - : componentSnapshot.DesktopClockTimeZoneId.Trim(); - _secondHandMode = ClockSecondHandMode.Normalize(componentSnapshot.DesktopClockSecondHandMode); - - _allTimeZones = _timeZoneService - .GetAllTimeZones() - .OrderBy(zone => zone.GetUtcOffset(DateTime.UtcNow)) - .ThenBy(zone => zone.DisplayName, StringComparer.OrdinalIgnoreCase) - .ToList(); - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("desktop_clock.settings.title", "时钟设置"); - DescriptionTextBlock.Text = L("desktop_clock.settings.desc", "为单时钟选择时区。"); - TimeZoneLabelTextBlock.Text = L("desktop_clock.settings.timezone_label", "时区"); - SecondHandModeLabelTextBlock.Text = L("desktop_clock.settings.second_mode_label", "秒针方式"); - SecondHandTickRadioButton.Content = L("clock.second_mode.tick", "跳针"); - SecondHandSweepRadioButton.Content = L("clock.second_mode.sweep", "扫针"); - } - - private void PopulateTimeZoneComboBox() - { - _suppressEvents = true; - try - { - TimeZoneComboBox.Items.Clear(); - foreach (var timeZone in _allTimeZones) - { - TimeZoneComboBox.Items.Add(new ComboBoxItem - { - Tag = timeZone.Id, - Content = GetLocalizedTimeZoneDisplayName(timeZone) - }); - } - - var normalizedId = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds( - new[] { _selectedTimeZoneId }, - _allTimeZones)[0]; - _selectedTimeZoneId = normalizedId; - - var selected = TimeZoneComboBox.Items - .OfType() - .FirstOrDefault(item => string.Equals(item.Tag as string, normalizedId, StringComparison.OrdinalIgnoreCase)); - - TimeZoneComboBox.SelectedItem = selected ?? TimeZoneComboBox.Items.OfType().FirstOrDefault(); - - var normalizedMode = ClockSecondHandMode.Normalize(_secondHandMode); - SecondHandTickRadioButton.IsChecked = string.Equals( - normalizedMode, - ClockSecondHandMode.Tick, - StringComparison.OrdinalIgnoreCase); - SecondHandSweepRadioButton.IsChecked = string.Equals( - normalizedMode, - ClockSecondHandMode.Sweep, - StringComparison.OrdinalIgnoreCase); - } - finally - { - _suppressEvents = false; - } - } - - private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void OnSecondHandModeChanged(object? sender, RoutedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void SaveState() - { - var selectedId = (TimeZoneComboBox.SelectedItem as ComboBoxItem)?.Tag as string; - var normalizedId = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds( - new[] { selectedId ?? _selectedTimeZoneId }, - _allTimeZones)[0]; - _selectedTimeZoneId = normalizedId; - _secondHandMode = GetSelectedSecondHandMode(); - - var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); - snapshot.DesktopClockTimeZoneId = normalizedId; - snapshot.DesktopClockSecondHandMode = _secondHandMode; - _componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot); - - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private string GetSelectedSecondHandMode() - { - return SecondHandSweepRadioButton.IsChecked == true - ? ClockSecondHandMode.Sweep - : ClockSecondHandMode.Tick; - } - - private string GetLocalizedTimeZoneDisplayName(TimeZoneInfo timeZone) - { - var offset = timeZone.GetUtcOffset(DateTime.UtcNow); - var sign = offset >= TimeSpan.Zero ? "+" : "-"; - var totalMinutes = Math.Abs((int)offset.TotalMinutes); - var hours = totalMinutes / 60; - var minutes = totalMinutes % 60; - - var displayName = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) - ? ResolveZhDisplayName(timeZone) - : ResolveEnDisplayName(timeZone); - - return $"(UTC{sign}{hours:D2}:{minutes:D2}) {displayName}"; - } - - private static string ResolveZhDisplayName(TimeZoneInfo timeZone) - { - if (ZhTimeZoneNames.TryGetValue(timeZone.Id, out var localizedName)) - { - return localizedName; - } - - return string.IsNullOrWhiteSpace(timeZone.StandardName) - ? timeZone.DisplayName - : timeZone.StandardName; - } - - private static string ResolveEnDisplayName(TimeZoneInfo timeZone) - { - if (!string.IsNullOrWhiteSpace(timeZone.StandardName)) - { - return timeZone.StandardName; - } - - return timeZone.DisplayName; - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } -} diff --git a/LanMountainDesktop/Views/Components/BaiduHotSearchSettingsWindow.axaml b/LanMountainDesktop/Views/Components/BaiduHotSearchSettingsWindow.axaml deleted file mode 100644 index a4b2a75..0000000 --- a/LanMountainDesktop/Views/Components/BaiduHotSearchSettingsWindow.axaml +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/BaiduHotSearchSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/BaiduHotSearchSettingsWindow.axaml.cs deleted file mode 100644 index c73a27f..0000000 --- a/LanMountainDesktop/Views/Components/BaiduHotSearchSettingsWindow.axaml.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using LanMountainDesktop.Models; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class BaiduHotSearchSettingsWindow : UserControl -{ - private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes; - - private readonly AppSettingsService _appSettingsService = new(); - private readonly ComponentSettingsService _componentSettingsService = new(); - private readonly LocalizationService _localizationService = new(); - private bool _suppressEvents; - private string _languageCode = "zh-CN"; - - public event EventHandler? SettingsChanged; - - public BaiduHotSearchSettingsWindow() - { - InitializeComponent(); - InitializeFrequencyOptions(); - LoadState(); - ApplyLocalization(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsService.Load(); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - - var sourceType = BaiduHotSearchSourceTypes.Normalize(componentSnapshot.BaiduHotSearchSourceType); - var enabled = componentSnapshot.BaiduHotSearchAutoRefreshEnabled; - var interval = NormalizeInterval(componentSnapshot.BaiduHotSearchAutoRefreshIntervalMinutes); - - _suppressEvents = true; - SelectSourceType(sourceType); - AutoRefreshCheckBox.IsChecked = enabled; - SelectInterval(interval); - FrequencyCardBorder.IsVisible = enabled; - _suppressEvents = false; - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("baiduhot.settings.title", "Baidu hot search settings"); - DescriptionTextBlock.Text = L("baiduhot.settings.desc", "Configure source, auto refresh and refresh interval."); - SourceLabelTextBlock.Text = L("baiduhot.settings.source_label", "Data source"); - SourceOfficialItem.Content = L("baiduhot.settings.source_official", "Official Source"); - SourceThirdPartyRssItem.Content = L("baiduhot.settings.source_rss", "Third-party RSS"); - AutoRefreshLabelTextBlock.Text = L("baiduhot.settings.auto_refresh_label", "Auto refresh"); - AutoRefreshCheckBox.Content = L("baiduhot.settings.auto_refresh_enabled", "Enable auto refresh"); - FrequencyLabelTextBlock.Text = L("baiduhot.settings.frequency_label", "Refresh interval"); - ApplyFrequencyLocalization(); - } - - private void OnSourceSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - var enabled = AutoRefreshCheckBox.IsChecked == true; - FrequencyCardBorder.IsVisible = enabled; - SaveState(); - } - - private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void SaveState() - { - var snapshot = _componentSettingsService.Load(); - snapshot.BaiduHotSearchSourceType = GetSelectedSourceType(); - snapshot.BaiduHotSearchAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true; - snapshot.BaiduHotSearchAutoRefreshIntervalMinutes = GetSelectedInterval(); - _componentSettingsService.Save(snapshot); - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private string GetSelectedSourceType() - { - if (SourceComboBox.SelectedItem is ComboBoxItem item && - item.Tag is string sourceTag) - { - return BaiduHotSearchSourceTypes.Normalize(sourceTag); - } - - return BaiduHotSearchSourceTypes.Official; - } - - private int GetSelectedInterval() - { - if (FrequencyComboBox.SelectedItem is ComboBoxItem item && - item.Tag is string tagText && - int.TryParse(tagText, out var minutes)) - { - return NormalizeInterval(minutes); - } - - return 15; - } - - private void SelectSourceType(string sourceType) - { - var normalizedSourceType = BaiduHotSearchSourceTypes.Normalize(sourceType); - var selected = SourceComboBox.Items - .OfType() - .FirstOrDefault(item => - item.Tag is string sourceTag && - string.Equals(BaiduHotSearchSourceTypes.Normalize(sourceTag), normalizedSourceType, StringComparison.OrdinalIgnoreCase)); - SourceComboBox.SelectedItem = selected ?? SourceComboBox.Items.OfType().FirstOrDefault(); - } - - private void SelectInterval(int intervalMinutes) - { - var selected = FrequencyComboBox.Items - .OfType() - .FirstOrDefault(item => - item.Tag is string tagText && - int.TryParse(tagText, out var minutes) && - minutes == intervalMinutes); - FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType().FirstOrDefault(); - } - - private static int NormalizeInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 15); - } - - private void InitializeFrequencyOptions() - { - FrequencyComboBox.Items.Clear(); - foreach (var minutes in SupportedIntervals) - { - FrequencyComboBox.Items.Add(new ComboBoxItem - { - Tag = minutes.ToString(), - Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes) - }); - } - } - - private void ApplyFrequencyLocalization() - { - foreach (var item in FrequencyComboBox.Items.OfType()) - { - if (item.Tag is not string tagText || - !int.TryParse(tagText, out var minutes)) - { - continue; - } - - var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}"; - item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)); - } - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } -} diff --git a/LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml b/LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml deleted file mode 100644 index 902548b..0000000 --- a/LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml.cs deleted file mode 100644 index b596daa..0000000 --- a/LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using LanMountainDesktop.Models; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class BilibiliHotSearchSettingsWindow : UserControl -{ - private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes; - - private readonly AppSettingsService _appSettingsService = new(); - private readonly ComponentSettingsService _componentSettingsService = new(); - private readonly LocalizationService _localizationService = new(); - private bool _suppressEvents; - private string _languageCode = "zh-CN"; - - public event EventHandler? SettingsChanged; - - public BilibiliHotSearchSettingsWindow() - { - InitializeComponent(); - InitializeFrequencyOptions(); - LoadState(); - ApplyLocalization(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsService.Load(); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - - var enabled = componentSnapshot.BilibiliHotSearchAutoRefreshEnabled; - var interval = NormalizeInterval(componentSnapshot.BilibiliHotSearchAutoRefreshIntervalMinutes); - - _suppressEvents = true; - AutoRefreshCheckBox.IsChecked = enabled; - SelectInterval(interval); - FrequencyCardBorder.IsVisible = enabled; - _suppressEvents = false; - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("bilihot.settings.title", "Bilibili hot search settings"); - DescriptionTextBlock.Text = L("bilihot.settings.desc", "Configure auto refresh and refresh interval."); - AutoRefreshLabelTextBlock.Text = L("bilihot.settings.auto_refresh_label", "Auto refresh"); - AutoRefreshCheckBox.Content = L("bilihot.settings.auto_refresh_enabled", "Enable auto refresh"); - FrequencyLabelTextBlock.Text = L("bilihot.settings.frequency_label", "Refresh interval"); - ApplyFrequencyLocalization(); - } - - private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - var enabled = AutoRefreshCheckBox.IsChecked == true; - FrequencyCardBorder.IsVisible = enabled; - SaveState(); - } - - private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void SaveState() - { - var snapshot = _componentSettingsService.Load(); - snapshot.BilibiliHotSearchAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true; - snapshot.BilibiliHotSearchAutoRefreshIntervalMinutes = GetSelectedInterval(); - _componentSettingsService.Save(snapshot); - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private int GetSelectedInterval() - { - if (FrequencyComboBox.SelectedItem is ComboBoxItem item && - item.Tag is string tagText && - int.TryParse(tagText, out var minutes)) - { - return NormalizeInterval(minutes); - } - - return 15; - } - - private void SelectInterval(int intervalMinutes) - { - var selected = FrequencyComboBox.Items - .OfType() - .FirstOrDefault(item => - item.Tag is string tagText && - int.TryParse(tagText, out var minutes) && - minutes == intervalMinutes); - FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType().FirstOrDefault(); - } - - private static int NormalizeInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 15); - } - - private void InitializeFrequencyOptions() - { - FrequencyComboBox.Items.Clear(); - foreach (var minutes in SupportedIntervals) - { - FrequencyComboBox.Items.Add(new ComboBoxItem - { - Tag = minutes.ToString(), - Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes) - }); - } - } - - private void ApplyFrequencyLocalization() - { - foreach (var item in FrequencyComboBox.Items.OfType()) - { - if (item.Tag is not string tagText || - !int.TryParse(tagText, out var minutes)) - { - continue; - } - - var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}"; - item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)); - } - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } -} diff --git a/LanMountainDesktop/Views/Components/ClassScheduleSettingsWindow.axaml b/LanMountainDesktop/Views/Components/ClassScheduleSettingsWindow.axaml deleted file mode 100644 index 0f7119d..0000000 --- a/LanMountainDesktop/Views/Components/ClassScheduleSettingsWindow.axaml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/ClassScheduleSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/ClassScheduleSettingsWindow.axaml.cs deleted file mode 100644 index 1b04221..0000000 --- a/LanMountainDesktop/Views/Components/ClassScheduleSettingsWindow.axaml.cs +++ /dev/null @@ -1,369 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Platform.Storage; -using LanMountainDesktop.ComponentSystem; -using LanMountainDesktop.Models; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class ClassScheduleSettingsWindow : UserControl, IComponentPlacementContextAware, IComponentSettingsStoreAware -{ - private readonly AppSettingsService _appSettingsService = new(); - private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService(); - private readonly LocalizationService _localizationService = new(); - private readonly List _importedSchedules = []; - private string _activeScheduleId = string.Empty; - private string _languageCode = "zh-CN"; - private string _componentId = BuiltInComponentIds.DesktopClassSchedule; - private string _placementId = string.Empty; - - public event EventHandler? SettingsChanged; - - public ClassScheduleSettingsWindow() - { - InitializeComponent(); - LoadState(); - ApplyLocalization(); - RenderImportedSchedules(); - } - - public void SetComponentPlacementContext(string componentId, string? placementId) - { - _componentId = string.IsNullOrWhiteSpace(componentId) - ? BuiltInComponentIds.DesktopClassSchedule - : componentId.Trim(); - _placementId = placementId?.Trim() ?? string.Empty; - LoadState(); - ApplyLocalization(); - RenderImportedSchedules(); - } - - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - LoadState(); - ApplyLocalization(); - RenderImportedSchedules(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - - _importedSchedules.Clear(); - foreach (var item in componentSnapshot.ImportedClassSchedules) - { - if (string.IsNullOrWhiteSpace(item.Id) || - string.IsNullOrWhiteSpace(item.FilePath)) - { - continue; - } - - _importedSchedules.Add(new ImportedClassScheduleSnapshot - { - Id = item.Id.Trim(), - DisplayName = item.DisplayName?.Trim() ?? string.Empty, - FilePath = item.FilePath.Trim() - }); - } - - _activeScheduleId = componentSnapshot.ActiveImportedClassScheduleId?.Trim() ?? string.Empty; - if (_importedSchedules.Count > 0 && - !_importedSchedules.Any(item => string.Equals(item.Id, _activeScheduleId, StringComparison.OrdinalIgnoreCase))) - { - _activeScheduleId = _importedSchedules[0].Id; - } - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("schedule.settings.title", "课表导入"); - DescriptionTextBlock.Text = L( - "schedule.settings.desc", - "导入 ClassIsland 的 CSES 课表文件并选择启用项。"); - AddScheduleButtonTextBlock.Text = L("schedule.settings.add", "添加课表"); - EmptyStateTextBlock.Text = L("schedule.settings.empty", "暂无导入课表"); - } - - private async void OnAddScheduleClick(object? sender, RoutedEventArgs e) - { - var topLevel = TopLevel.GetTopLevel(this); - var storageProvider = topLevel?.StorageProvider; - if (storageProvider is null) - { - return; - } - - var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = L("schedule.settings.picker_title", "选择 ClassIsland 课表文件"), - AllowMultiple = false, - FileTypeFilter = - [ - new FilePickerFileType(L("schedule.settings.picker_file_type", "ClassIsland CSES 课表")) - { - Patterns = ["*.cses", "*.yaml", "*.yml"] - } - ] - }); - - if (files.Count == 0) - { - return; - } - - var importedPath = await ImportScheduleFileAsync(files[0]); - if (string.IsNullOrWhiteSpace(importedPath)) - { - return; - } - - var existing = _importedSchedules.FirstOrDefault(item => - string.Equals(item.FilePath, importedPath, StringComparison.OrdinalIgnoreCase)); - if (existing is not null) - { - _activeScheduleId = existing.Id; - SaveState(); - RenderImportedSchedules(); - return; - } - - var displayName = Path.GetFileNameWithoutExtension(importedPath)?.Trim(); - if (string.IsNullOrWhiteSpace(displayName)) - { - displayName = L("schedule.settings.unnamed", "未命名课表"); - } - - var imported = new ImportedClassScheduleSnapshot - { - Id = Guid.NewGuid().ToString("N"), - DisplayName = displayName, - FilePath = importedPath - }; - - _importedSchedules.Add(imported); - _activeScheduleId = imported.Id; - SaveState(); - RenderImportedSchedules(); - } - - private async Task ImportScheduleFileAsync(IStorageFile file) - { - try - { - var extension = Path.GetExtension(file.Name); - if (string.IsNullOrWhiteSpace(extension)) - { - extension = ".cses"; - } - - var importedDirectory = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "LanMountainDesktop", - "Schedules"); - Directory.CreateDirectory(importedDirectory); - - var destinationPath = Path.Combine( - importedDirectory, - $"{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}{extension}"); - - await using var sourceStream = await file.OpenReadAsync(); - await using var destinationStream = File.Create(destinationPath); - await sourceStream.CopyToAsync(destinationStream); - return destinationPath; - } - catch - { - return null; - } - } - - private void RenderImportedSchedules() - { - ScheduleItemsPanel.Children.Clear(); - - if (_importedSchedules.Count == 0) - { - EmptyStateTextBlock.IsVisible = true; - return; - } - - EmptyStateTextBlock.IsVisible = false; - foreach (var item in _importedSchedules) - { - var selector = new RadioButton - { - GroupName = "class_schedule_imports", - IsChecked = string.Equals(item.Id, _activeScheduleId, StringComparison.OrdinalIgnoreCase), - VerticalAlignment = VerticalAlignment.Center, - Tag = item.Id - }; - selector.IsCheckedChanged += OnScheduleSelectionChanged; - - var title = new TextBlock - { - Text = string.IsNullOrWhiteSpace(item.DisplayName) - ? L("schedule.settings.unnamed", "未命名课表") - : item.DisplayName, - FontSize = 14, - FontWeight = FontWeight.SemiBold, - Foreground = ResolveThemeBrush("AdaptiveTextPrimaryBrush", "#FFEFF3FF"), - TextTrimming = TextTrimming.CharacterEllipsis - }; - - var path = new TextBlock - { - Text = item.FilePath, - FontSize = 11, - Foreground = ResolveThemeBrush("AdaptiveTextSecondaryBrush", "#FF99A2B5"), - TextTrimming = TextTrimming.CharacterEllipsis, - TextWrapping = TextWrapping.NoWrap - }; - - var textStack = new StackPanel - { - Spacing = 4, - VerticalAlignment = VerticalAlignment.Center, - Children = { title, path } - }; - - var deleteButton = new Button - { - Content = L("schedule.settings.delete", "删除"), - Tag = item.Id, - Padding = new Thickness(10, 6), - MinWidth = 64, - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Center - }; - deleteButton.Click += OnDeleteScheduleClick; - - var rowGrid = new Grid - { - ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"), - ColumnSpacing = 10 - }; - rowGrid.Children.Add(selector); - rowGrid.Children.Add(textStack); - rowGrid.Children.Add(deleteButton); - Grid.SetColumn(selector, 0); - Grid.SetColumn(textStack, 1); - Grid.SetColumn(deleteButton, 2); - - var rowBorder = new Border - { - Padding = new Thickness(10, 8), - CornerRadius = new CornerRadius(12), - Background = ResolveThemeBrush("AdaptiveSurfaceRaisedBrush", "#1AFFFFFF"), - BorderBrush = ResolveThemeBrush("AdaptiveButtonBorderBrush", "#22000000"), - BorderThickness = new Thickness(1), - Child = rowGrid - }; - - ScheduleItemsPanel.Children.Add(rowBorder); - } - } - - private void OnScheduleSelectionChanged(object? sender, RoutedEventArgs e) - { - if (sender is not RadioButton button || - button.IsChecked != true || - button.Tag is not string scheduleId) - { - return; - } - - if (string.Equals(_activeScheduleId, scheduleId, StringComparison.OrdinalIgnoreCase)) - { - return; - } - - _activeScheduleId = scheduleId; - SaveState(); - } - - private void OnDeleteScheduleClick(object? sender, RoutedEventArgs e) - { - if (sender is not Button button || button.Tag is not string scheduleId) - { - return; - } - - var target = _importedSchedules.FirstOrDefault(item => - string.Equals(item.Id, scheduleId, StringComparison.OrdinalIgnoreCase)); - if (target is null) - { - return; - } - - _importedSchedules.Remove(target); - TryDeleteImportedFile(target.FilePath); - if (string.Equals(_activeScheduleId, scheduleId, StringComparison.OrdinalIgnoreCase)) - { - _activeScheduleId = _importedSchedules.Count > 0 ? _importedSchedules[0].Id : string.Empty; - } - - SaveState(); - RenderImportedSchedules(); - } - - private void SaveState() - { - var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); - snapshot.ImportedClassSchedules = _importedSchedules - .Select(item => new ImportedClassScheduleSnapshot - { - Id = item.Id, - DisplayName = item.DisplayName, - FilePath = item.FilePath - }) - .ToList(); - snapshot.ActiveImportedClassScheduleId = _activeScheduleId ?? string.Empty; - _componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot); - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } - - private static void TryDeleteImportedFile(string? filePath) - { - if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) - { - return; - } - - try - { - File.Delete(filePath); - } - catch - { - // Keep settings operation resilient even when file deletion fails. - } - } - - private IBrush ResolveThemeBrush(string key, string fallbackHex) - { - if (this.TryFindResource(key, out var value) && value is IBrush brush) - { - return brush; - } - - return new SolidColorBrush(Color.Parse(fallbackHex)); - } -} diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml b/LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml deleted file mode 100644 index 51c7f18..0000000 --- a/LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml.cs deleted file mode 100644 index f0d0de1..0000000 --- a/LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using LanMountainDesktop.Models; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class CnrDailyNewsSettingsWindow : UserControl -{ - private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes; - - private readonly AppSettingsService _appSettingsService = new(); - private readonly ComponentSettingsService _componentSettingsService = new(); - private readonly LocalizationService _localizationService = new(); - private bool _suppressEvents; - private string _languageCode = "zh-CN"; - - public event EventHandler? SettingsChanged; - - public CnrDailyNewsSettingsWindow() - { - InitializeComponent(); - InitializeFrequencyOptions(); - LoadState(); - ApplyLocalization(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsService.Load(); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - - var enabled = componentSnapshot.CnrDailyNewsAutoRotateEnabled; - var interval = NormalizeInterval(componentSnapshot.CnrDailyNewsAutoRotateIntervalMinutes); - - _suppressEvents = true; - AutoRotateCheckBox.IsChecked = enabled; - SelectInterval(interval); - FrequencyCardBorder.IsVisible = enabled; - _suppressEvents = false; - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("cnrnews.settings.title", "CNR news settings"); - DescriptionTextBlock.Text = L("cnrnews.settings.desc", "Configure auto-rotation and refresh interval."); - AutoRotateLabelTextBlock.Text = L("cnrnews.settings.auto_rotate_label", "Auto-rotation"); - AutoRotateCheckBox.Content = L("cnrnews.settings.auto_rotate_enabled", "Enable auto-rotation"); - FrequencyLabelTextBlock.Text = L("cnrnews.settings.frequency_label", "Rotation interval"); - ApplyFrequencyLocalization(); - } - - private void OnAutoRotateChanged(object? sender, RoutedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - var enabled = AutoRotateCheckBox.IsChecked == true; - FrequencyCardBorder.IsVisible = enabled; - SaveState(); - } - - private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void SaveState() - { - var snapshot = _componentSettingsService.Load(); - snapshot.CnrDailyNewsAutoRotateEnabled = AutoRotateCheckBox.IsChecked == true; - snapshot.CnrDailyNewsAutoRotateIntervalMinutes = GetSelectedInterval(); - _componentSettingsService.Save(snapshot); - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private int GetSelectedInterval() - { - if (FrequencyComboBox.SelectedItem is ComboBoxItem item && - item.Tag is string tagText && - int.TryParse(tagText, out var minutes)) - { - return NormalizeInterval(minutes); - } - - return 60; - } - - private void SelectInterval(int intervalMinutes) - { - var selected = FrequencyComboBox.Items - .OfType() - .FirstOrDefault(item => - item.Tag is string tagText && - int.TryParse(tagText, out var minutes) && - minutes == intervalMinutes); - FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType().FirstOrDefault(); - } - - private static int NormalizeInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 60); - } - - private void InitializeFrequencyOptions() - { - FrequencyComboBox.Items.Clear(); - foreach (var minutes in SupportedIntervals) - { - FrequencyComboBox.Items.Add(new ComboBoxItem - { - Tag = minutes.ToString(), - Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes) - }); - } - } - - private void ApplyFrequencyLocalization() - { - foreach (var item in FrequencyComboBox.Items.OfType()) - { - if (item.Tag is not string tagText || - !int.TryParse(tagText, out var minutes)) - { - continue; - } - - var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}"; - item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)); - } - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } -} diff --git a/LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml b/LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml deleted file mode 100644 index 7b590c0..0000000 --- a/LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml.cs deleted file mode 100644 index a07c9c7..0000000 --- a/LanMountainDesktop/Views/Components/DailyArtworkSettingsWindow.axaml.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using LanMountainDesktop.ComponentSystem; -using LanMountainDesktop.Models; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class DailyArtworkSettingsWindow : UserControl, IComponentPlacementContextAware, IComponentSettingsStoreAware -{ - private readonly AppSettingsService _appSettingsService = new(); - private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService(); - private readonly LocalizationService _localizationService = new(); - private string _languageCode = "zh-CN"; - private bool _suppressEvents; - private string _componentId = BuiltInComponentIds.DesktopDailyArtwork; - private string _placementId = string.Empty; - - public event EventHandler? SettingsChanged; - - public DailyArtworkSettingsWindow() - { - InitializeComponent(); - LoadState(); - ApplyLocalization(); - } - - public void SetComponentPlacementContext(string componentId, string? placementId) - { - _componentId = string.IsNullOrWhiteSpace(componentId) - ? BuiltInComponentIds.DesktopDailyArtwork - : componentId.Trim(); - _placementId = placementId?.Trim() ?? string.Empty; - LoadState(); - ApplyLocalization(); - } - - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - LoadState(); - ApplyLocalization(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - - var source = DailyArtworkMirrorSources.Normalize(componentSnapshot.DailyArtworkMirrorSource); - _suppressEvents = true; - MirrorSourceComboBox.SelectedIndex = string.Equals(source, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase) - ? 0 - : 1; - _suppressEvents = false; - UpdateSourceStatus(source); - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("artwork.settings.title", "每日图片设置"); - DescriptionTextBlock.Text = L("artwork.settings.desc", "切换每日图片的数据源。"); - MirrorSourceLabelTextBlock.Text = L("artwork.settings.source_label", "镜像源"); - MirrorSourceDomesticItem.Content = L("artwork.settings.source_domestic", "国内镜像"); - MirrorSourceOverseasItem.Content = L("artwork.settings.source_overseas", "国外镜像"); - UpdateSourceStatus(GetSelectedSource()); - } - - private void OnMirrorSourceSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - var source = GetSelectedSource(); - var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); - snapshot.DailyArtworkMirrorSource = source; - _componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot); - - UpdateSourceStatus(source); - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private string GetSelectedSource() - { - if (MirrorSourceComboBox.SelectedItem is ComboBoxItem comboBoxItem && - comboBoxItem.Tag is string tagValue) - { - return DailyArtworkMirrorSources.Normalize(tagValue); - } - - return DailyArtworkMirrorSources.Overseas; - } - - private void UpdateSourceStatus(string source) - { - if (StatusTextBlock is null) - { - return; - } - - StatusTextBlock.Text = string.Equals(source, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase) - ? L("artwork.settings.source_status_domestic", "当前源:国内镜像(优先中国网络)") - : L("artwork.settings.source_status_overseas", "当前源:国外镜像(艺术馆推荐)"); - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } -} diff --git a/LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml b/LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml deleted file mode 100644 index 7fb9c55..0000000 --- a/LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml.cs deleted file mode 100644 index 4e0d80a..0000000 --- a/LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using LanMountainDesktop.Models; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class DailyWordSettingsWindow : UserControl -{ - private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes; - - private readonly AppSettingsService _appSettingsService = new(); - private readonly ComponentSettingsService _componentSettingsService = new(); - private readonly LocalizationService _localizationService = new(); - private bool _suppressEvents; - private string _languageCode = "zh-CN"; - - public event EventHandler? SettingsChanged; - - public DailyWordSettingsWindow() - { - InitializeComponent(); - InitializeFrequencyOptions(); - LoadState(); - ApplyLocalization(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsService.Load(); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - - var enabled = componentSnapshot.DailyWordAutoRefreshEnabled; - var interval = NormalizeInterval(componentSnapshot.DailyWordAutoRefreshIntervalMinutes); - - _suppressEvents = true; - AutoRefreshCheckBox.IsChecked = enabled; - SelectInterval(interval); - FrequencyCardBorder.IsVisible = enabled; - _suppressEvents = false; - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("dailyword.settings.title", "Daily word settings"); - DescriptionTextBlock.Text = L("dailyword.settings.desc", "Configure auto refresh and refresh interval."); - AutoRefreshLabelTextBlock.Text = L("dailyword.settings.auto_refresh_label", "Auto refresh"); - AutoRefreshCheckBox.Content = L("dailyword.settings.auto_refresh_enabled", "Enable auto refresh"); - FrequencyLabelTextBlock.Text = L("dailyword.settings.frequency_label", "Refresh interval"); - ApplyFrequencyLocalization(); - } - - private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - var enabled = AutoRefreshCheckBox.IsChecked == true; - FrequencyCardBorder.IsVisible = enabled; - SaveState(); - } - - private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void SaveState() - { - var snapshot = _componentSettingsService.Load(); - snapshot.DailyWordAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true; - snapshot.DailyWordAutoRefreshIntervalMinutes = GetSelectedInterval(); - _componentSettingsService.Save(snapshot); - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private int GetSelectedInterval() - { - if (FrequencyComboBox.SelectedItem is ComboBoxItem item && - item.Tag is string tagText && - int.TryParse(tagText, out var minutes)) - { - return NormalizeInterval(minutes); - } - - return 360; - } - - private void SelectInterval(int intervalMinutes) - { - var selected = FrequencyComboBox.Items - .OfType() - .FirstOrDefault(item => - item.Tag is string tagText && - int.TryParse(tagText, out var minutes) && - minutes == intervalMinutes); - FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType().FirstOrDefault(); - } - - private static int NormalizeInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 360); - } - - private void InitializeFrequencyOptions() - { - FrequencyComboBox.Items.Clear(); - foreach (var minutes in SupportedIntervals) - { - FrequencyComboBox.Items.Add(new ComboBoxItem - { - Tag = minutes.ToString(), - Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes) - }); - } - } - - private void ApplyFrequencyLocalization() - { - foreach (var item in FrequencyComboBox.Items.OfType()) - { - if (item.Tag is not string tagText || - !int.TryParse(tagText, out var minutes)) - { - continue; - } - - var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}"; - item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)); - } - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } -} diff --git a/LanMountainDesktop/Views/Components/DateWidgetSettingsWindow.axaml b/LanMountainDesktop/Views/Components/DateWidgetSettingsWindow.axaml deleted file mode 100644 index 397d588..0000000 --- a/LanMountainDesktop/Views/Components/DateWidgetSettingsWindow.axaml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/DateWidgetSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/DateWidgetSettingsWindow.axaml.cs deleted file mode 100644 index 757caa0..0000000 --- a/LanMountainDesktop/Views/Components/DateWidgetSettingsWindow.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace LanMountainDesktop.Views.Components; - -public partial class DateWidgetSettingsWindow : UserControl -{ - public DateWidgetSettingsWindow() - { - InitializeComponent(); - } -} diff --git a/LanMountainDesktop/Views/Components/IfengNewsSettingsWindow.axaml b/LanMountainDesktop/Views/Components/IfengNewsSettingsWindow.axaml deleted file mode 100644 index c18e226..0000000 --- a/LanMountainDesktop/Views/Components/IfengNewsSettingsWindow.axaml +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/IfengNewsSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/IfengNewsSettingsWindow.axaml.cs deleted file mode 100644 index a58acc3..0000000 --- a/LanMountainDesktop/Views/Components/IfengNewsSettingsWindow.axaml.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using LanMountainDesktop.Models; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class IfengNewsSettingsWindow : UserControl -{ - private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes; - - private readonly AppSettingsService _appSettingsService = new(); - private readonly ComponentSettingsService _componentSettingsService = new(); - private readonly LocalizationService _localizationService = new(); - private bool _suppressEvents; - private string _languageCode = "zh-CN"; - - public event EventHandler? SettingsChanged; - - public IfengNewsSettingsWindow() - { - InitializeComponent(); - InitializeFrequencyOptions(); - LoadState(); - ApplyLocalization(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsService.Load(); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - - var channelType = IfengNewsChannelTypes.Normalize(componentSnapshot.IfengNewsChannelType); - var enabled = componentSnapshot.IfengNewsAutoRefreshEnabled; - var interval = NormalizeInterval(componentSnapshot.IfengNewsAutoRefreshIntervalMinutes); - - _suppressEvents = true; - SelectChannelType(channelType); - AutoRefreshCheckBox.IsChecked = enabled; - SelectInterval(interval); - FrequencyCardBorder.IsVisible = enabled; - _suppressEvents = false; - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("ifeng.settings.title", "iFeng news settings"); - DescriptionTextBlock.Text = L("ifeng.settings.desc", "Configure channel, auto refresh and refresh interval."); - ChannelLabelTextBlock.Text = L("ifeng.settings.channel_label", "News channel"); - ChannelComprehensiveItem.Content = L("ifeng.settings.channel_comprehensive", "Comprehensive"); - ChannelMainlandItem.Content = L("ifeng.settings.channel_mainland", "China Mainland"); - ChannelTaiwanItem.Content = L("ifeng.settings.channel_taiwan", "Taiwan"); - AutoRefreshLabelTextBlock.Text = L("ifeng.settings.auto_refresh_label", "Auto refresh"); - AutoRefreshCheckBox.Content = L("ifeng.settings.auto_refresh_enabled", "Enable auto refresh"); - FrequencyLabelTextBlock.Text = L("ifeng.settings.frequency_label", "Refresh interval"); - ApplyFrequencyLocalization(); - } - - private void OnChannelSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - var enabled = AutoRefreshCheckBox.IsChecked == true; - FrequencyCardBorder.IsVisible = enabled; - SaveState(); - } - - private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void SaveState() - { - var snapshot = _componentSettingsService.Load(); - snapshot.IfengNewsChannelType = GetSelectedChannelType(); - snapshot.IfengNewsAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true; - snapshot.IfengNewsAutoRefreshIntervalMinutes = GetSelectedInterval(); - _componentSettingsService.Save(snapshot); - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private string GetSelectedChannelType() - { - if (ChannelComboBox.SelectedItem is ComboBoxItem item && - item.Tag is string channelTag) - { - return IfengNewsChannelTypes.Normalize(channelTag); - } - - return IfengNewsChannelTypes.Comprehensive; - } - - private int GetSelectedInterval() - { - if (FrequencyComboBox.SelectedItem is ComboBoxItem item && - item.Tag is string tagText && - int.TryParse(tagText, out var minutes)) - { - return NormalizeInterval(minutes); - } - - return 20; - } - - private void SelectChannelType(string channelType) - { - var normalizedChannelType = IfengNewsChannelTypes.Normalize(channelType); - var selected = ChannelComboBox.Items - .OfType() - .FirstOrDefault(item => - item.Tag is string channelTag && - string.Equals(IfengNewsChannelTypes.Normalize(channelTag), normalizedChannelType, StringComparison.OrdinalIgnoreCase)); - ChannelComboBox.SelectedItem = selected ?? ChannelComboBox.Items.OfType().FirstOrDefault(); - } - - private void SelectInterval(int intervalMinutes) - { - var selected = FrequencyComboBox.Items - .OfType() - .FirstOrDefault(item => - item.Tag is string tagText && - int.TryParse(tagText, out var minutes) && - minutes == intervalMinutes); - FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType().FirstOrDefault(); - } - - private static int NormalizeInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 20); - } - - private void InitializeFrequencyOptions() - { - FrequencyComboBox.Items.Clear(); - foreach (var minutes in SupportedIntervals) - { - FrequencyComboBox.Items.Add(new ComboBoxItem - { - Tag = minutes.ToString(), - Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes) - }); - } - } - - private void ApplyFrequencyLocalization() - { - foreach (var item in FrequencyComboBox.Items.OfType()) - { - if (item.Tag is not string tagText || - !int.TryParse(tagText, out var minutes)) - { - continue; - } - - var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}"; - item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)); - } - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } -} diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml b/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml deleted file mode 100644 index c93cfbc..0000000 --- a/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml.cs deleted file mode 100644 index 7c59334..0000000 --- a/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using LanMountainDesktop.Models; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class Stcn24ForumSettingsWindow : UserControl -{ - private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes; - - private readonly AppSettingsService _appSettingsService = new(); - private readonly ComponentSettingsService _componentSettingsService = new(); - private readonly LocalizationService _localizationService = new(); - private bool _suppressEvents; - private string _languageCode = "zh-CN"; - - public event EventHandler? SettingsChanged; - - public Stcn24ForumSettingsWindow() - { - InitializeComponent(); - InitializeFrequencyOptions(); - LoadState(); - ApplyLocalization(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsService.Load(); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - - var enabled = componentSnapshot.Stcn24ForumAutoRefreshEnabled; - var interval = NormalizeInterval(componentSnapshot.Stcn24ForumAutoRefreshIntervalMinutes); - var sourceType = Stcn24ForumSourceTypes.Normalize(componentSnapshot.Stcn24ForumSourceType); - - _suppressEvents = true; - AutoRefreshCheckBox.IsChecked = enabled; - SelectSourceType(sourceType); - SelectInterval(interval); - FrequencyCardBorder.IsVisible = enabled; - _suppressEvents = false; - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("stcn24.settings.title", "STCN 24 settings"); - DescriptionTextBlock.Text = L("stcn24.settings.desc", "Configure information source, auto refresh and refresh interval."); - SourceLabelTextBlock.Text = L("stcn24.settings.source_label", "Information source"); - SourceLatestCreatedItem.Content = L("stcn24.settings.source_latest_created", "Latest posts"); - SourceLatestActivityItem.Content = L("stcn24.settings.source_latest_activity", "Latest activity"); - SourceMostRepliesItem.Content = L("stcn24.settings.source_most_replies", "Most replies"); - SourceEarliestCreatedItem.Content = L("stcn24.settings.source_earliest_created", "Earliest posts"); - SourceEarliestActivityItem.Content = L("stcn24.settings.source_earliest_activity", "Earliest activity"); - SourceLeastRepliesItem.Content = L("stcn24.settings.source_least_replies", "Least replies"); - SourceFrontpageLatestItem.Content = L("stcn24.settings.source_frontpage_latest", "Frontpage latest"); - SourceFrontpageEarliestItem.Content = L("stcn24.settings.source_frontpage_earliest", "Frontpage earliest"); - AutoRefreshLabelTextBlock.Text = L("stcn24.settings.auto_refresh_label", "Auto refresh"); - AutoRefreshCheckBox.Content = L("stcn24.settings.auto_refresh_enabled", "Enable auto refresh"); - FrequencyLabelTextBlock.Text = L("stcn24.settings.frequency_label", "Refresh interval"); - ApplyFrequencyLocalization(); - } - - private void OnSourceSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - var enabled = AutoRefreshCheckBox.IsChecked == true; - FrequencyCardBorder.IsVisible = enabled; - SaveState(); - } - - private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void SaveState() - { - var snapshot = _componentSettingsService.Load(); - snapshot.Stcn24ForumSourceType = GetSelectedSourceType(); - snapshot.Stcn24ForumAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true; - snapshot.Stcn24ForumAutoRefreshIntervalMinutes = GetSelectedInterval(); - _componentSettingsService.Save(snapshot); - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private string GetSelectedSourceType() - { - if (SourceComboBox.SelectedItem is ComboBoxItem item && - item.Tag is string sourceTag) - { - return Stcn24ForumSourceTypes.Normalize(sourceTag); - } - - return Stcn24ForumSourceTypes.LatestCreated; - } - - private int GetSelectedInterval() - { - if (FrequencyComboBox.SelectedItem is ComboBoxItem item && - item.Tag is string tagText && - int.TryParse(tagText, out var minutes)) - { - return NormalizeInterval(minutes); - } - - return 20; - } - - private void SelectInterval(int intervalMinutes) - { - var selected = FrequencyComboBox.Items - .OfType() - .FirstOrDefault(item => - item.Tag is string tagText && - int.TryParse(tagText, out var minutes) && - minutes == intervalMinutes); - FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType().FirstOrDefault(); - } - - private void SelectSourceType(string sourceType) - { - var normalizedSourceType = Stcn24ForumSourceTypes.Normalize(sourceType); - var selected = SourceComboBox.Items - .OfType() - .FirstOrDefault(item => - item.Tag is string sourceTag && - string.Equals(Stcn24ForumSourceTypes.Normalize(sourceTag), normalizedSourceType, StringComparison.OrdinalIgnoreCase)); - SourceComboBox.SelectedItem = selected ?? SourceComboBox.Items.OfType().FirstOrDefault(); - } - - private static int NormalizeInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 20); - } - - private void InitializeFrequencyOptions() - { - FrequencyComboBox.Items.Clear(); - foreach (var minutes in SupportedIntervals) - { - FrequencyComboBox.Items.Add(new ComboBoxItem - { - Tag = minutes.ToString(), - Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes) - }); - } - } - - private void ApplyFrequencyLocalization() - { - foreach (var item in FrequencyComboBox.Items.OfType()) - { - if (item.Tag is not string tagText || - !int.TryParse(tagText, out var minutes)) - { - continue; - } - - var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}"; - item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)); - } - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } -} diff --git a/LanMountainDesktop/Views/Components/StudyEnvironmentWidgetSettingsWindow.axaml b/LanMountainDesktop/Views/Components/StudyEnvironmentWidgetSettingsWindow.axaml deleted file mode 100644 index 6d5bdcb..0000000 --- a/LanMountainDesktop/Views/Components/StudyEnvironmentWidgetSettingsWindow.axaml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/StudyEnvironmentWidgetSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/StudyEnvironmentWidgetSettingsWindow.axaml.cs deleted file mode 100644 index 4de9c4a..0000000 --- a/LanMountainDesktop/Views/Components/StudyEnvironmentWidgetSettingsWindow.axaml.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Interactivity; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class StudyEnvironmentWidgetSettingsWindow : UserControl -{ - private readonly AppSettingsService _appSettingsService = new(); - private readonly ComponentSettingsService _componentSettingsService = new(); - private readonly LocalizationService _localizationService = new(); - private string _languageCode = "zh-CN"; - private bool _suppressEvents; - - public event EventHandler? SettingsChanged; - - public StudyEnvironmentWidgetSettingsWindow() - { - InitializeComponent(); - LoadState(); - ApplyLocalization(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsService.Load(); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - - var showDisplayDb = componentSnapshot.StudyEnvironmentShowDisplayDb; - var showDbfs = componentSnapshot.StudyEnvironmentShowDbfs; - if (!showDisplayDb && !showDbfs) - { - showDisplayDb = true; - } - - _suppressEvents = true; - ShowDisplayDbCheckBox.IsChecked = showDisplayDb; - ShowDbfsCheckBox.IsChecked = showDbfs; - _suppressEvents = false; - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("study.environment.settings.title", "环境组件设置"); - DescriptionTextBlock.Text = L( - "study.environment.settings.desc", - "配置右侧实时噪音值显示内容。"); - ShowDisplayDbCheckBox.Content = L( - "study.environment.settings.show_display_db", - "显示 display dB"); - ShowDbfsCheckBox.Content = L( - "study.environment.settings.show_dbfs", - "显示 dBFS"); - HintTextBlock.Text = L( - "study.environment.settings.hint", - "至少启用一种显示方式。"); - } - - private void OnDisplayModeChanged(object? sender, RoutedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - var showDisplayDb = ShowDisplayDbCheckBox.IsChecked == true; - var showDbfs = ShowDbfsCheckBox.IsChecked == true; - if (!showDisplayDb && !showDbfs) - { - _suppressEvents = true; - ShowDisplayDbCheckBox.IsChecked = true; - _suppressEvents = false; - showDisplayDb = true; - } - - var snapshot = _componentSettingsService.Load(); - snapshot.StudyEnvironmentShowDisplayDb = showDisplayDb; - snapshot.StudyEnvironmentShowDbfs = showDbfs; - _componentSettingsService.Save(snapshot); - - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } -} diff --git a/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml b/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml deleted file mode 100644 index c8209bc..0000000 --- a/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml.cs deleted file mode 100644 index 83c71cf..0000000 --- a/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using LanMountainDesktop.ComponentSystem; -using LanMountainDesktop.Models; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class WeatherWidgetSettingsWindow : UserControl, IComponentPlacementContextAware, IComponentSettingsStoreAware -{ - private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes; - - private readonly AppSettingsService _appSettingsService = new(); - private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService(); - private readonly LocalizationService _localizationService = new(); - private bool _suppressEvents; - private string _languageCode = "zh-CN"; - private string _componentId = BuiltInComponentIds.DesktopWeather; - private string _placementId = string.Empty; - - public event EventHandler? SettingsChanged; - - public WeatherWidgetSettingsWindow() - { - InitializeComponent(); - InitializeFrequencyOptions(); - LoadState(); - ApplyLocalization(); - } - - public void SetComponentPlacementContext(string componentId, string? placementId) - { - _componentId = string.IsNullOrWhiteSpace(componentId) - ? BuiltInComponentIds.DesktopWeather - : componentId.Trim(); - _placementId = placementId?.Trim() ?? string.Empty; - LoadState(); - ApplyLocalization(); - } - - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - LoadState(); - ApplyLocalization(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - - var enabled = componentSnapshot.WeatherAutoRefreshEnabled; - var interval = NormalizeInterval(componentSnapshot.WeatherAutoRefreshIntervalMinutes); - - _suppressEvents = true; - AutoRefreshCheckBox.IsChecked = enabled; - SelectInterval(interval); - FrequencyCardBorder.IsVisible = enabled; - _suppressEvents = false; - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("weather.widget.settings.title", "Weather widget settings"); - DescriptionTextBlock.Text = L("weather.widget.settings.desc", "Configure auto refresh and refresh interval for all weather widgets."); - AutoRefreshLabelTextBlock.Text = L("weather.widget.settings.auto_refresh_label", "Auto refresh"); - AutoRefreshCheckBox.Content = L("weather.widget.settings.auto_refresh_enabled", "Enable auto refresh"); - FrequencyLabelTextBlock.Text = L("weather.widget.settings.frequency_label", "Refresh interval"); - ApplyFrequencyLocalization(); - } - - private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - var enabled = AutoRefreshCheckBox.IsChecked == true; - FrequencyCardBorder.IsVisible = enabled; - SaveState(); - } - - private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void SaveState() - { - var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); - snapshot.WeatherAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true; - snapshot.WeatherAutoRefreshIntervalMinutes = GetSelectedInterval(); - _componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot); - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private int GetSelectedInterval() - { - if (FrequencyComboBox.SelectedItem is ComboBoxItem item && - item.Tag is string tagText && - int.TryParse(tagText, out var minutes)) - { - return NormalizeInterval(minutes); - } - - return 12; - } - - private void SelectInterval(int intervalMinutes) - { - var selected = FrequencyComboBox.Items - .OfType() - .FirstOrDefault(item => - item.Tag is string tagText && - int.TryParse(tagText, out var minutes) && - minutes == intervalMinutes); - FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType().FirstOrDefault(); - } - - private static int NormalizeInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 12); - } - - private void InitializeFrequencyOptions() - { - FrequencyComboBox.Items.Clear(); - foreach (var minutes in SupportedIntervals) - { - FrequencyComboBox.Items.Add(new ComboBoxItem - { - Tag = minutes.ToString(), - Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes) - }); - } - } - - private void ApplyFrequencyLocalization() - { - foreach (var item in FrequencyComboBox.Items.OfType()) - { - if (item.Tag is not string tagText || - !int.TryParse(tagText, out var minutes)) - { - continue; - } - - var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}"; - item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)); - } - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } -} diff --git a/LanMountainDesktop/Views/Components/WorldClockWidgetSettingsWindow.axaml b/LanMountainDesktop/Views/Components/WorldClockWidgetSettingsWindow.axaml deleted file mode 100644 index 9c0b6ff..0000000 --- a/LanMountainDesktop/Views/Components/WorldClockWidgetSettingsWindow.axaml +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/Components/WorldClockWidgetSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/WorldClockWidgetSettingsWindow.axaml.cs deleted file mode 100644 index 17e360e..0000000 --- a/LanMountainDesktop/Views/Components/WorldClockWidgetSettingsWindow.axaml.cs +++ /dev/null @@ -1,268 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Interactivity; -using LanMountainDesktop.ComponentSystem; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views.Components; - -public partial class WorldClockWidgetSettingsWindow : UserControl, IComponentPlacementContextAware, IComponentSettingsStoreAware -{ - private static readonly IReadOnlyDictionary ZhTimeZoneNames = - new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["China Standard Time"] = "中国标准时间", - ["Asia/Shanghai"] = "中国标准时间", - ["GMT Standard Time"] = "格林威治标准时间", - ["Europe/London"] = "格林威治标准时间", - ["AUS Eastern Standard Time"] = "澳大利亚东部标准时间", - ["Australia/Sydney"] = "澳大利亚东部标准时间", - ["Eastern Standard Time"] = "美国东部标准时间", - ["America/New_York"] = "美国东部标准时间", - ["Tokyo Standard Time"] = "日本标准时间", - ["Asia/Tokyo"] = "日本标准时间", - ["UTC"] = "协调世界时", - ["Etc/UTC"] = "协调世界时" - }; - - private readonly AppSettingsService _appSettingsService = new(); - private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService(); - private readonly LocalizationService _localizationService = new(); - private readonly TimeZoneService _timeZoneService = new(); - private readonly ComboBox[] _timeZoneComboBoxes; - private bool _suppressEvents; - private string _languageCode = "zh-CN"; - private string _componentId = BuiltInComponentIds.DesktopWorldClock; - private string _placementId = string.Empty; - private IReadOnlyList _allTimeZones = Array.Empty(); - private IReadOnlyList _selectedTimeZoneIds = Array.Empty(); - private string _secondHandMode = ClockSecondHandMode.Tick; - - public event EventHandler? SettingsChanged; - - public WorldClockWidgetSettingsWindow() - { - InitializeComponent(); - - _timeZoneComboBoxes = - [ - ClockOneTimeZoneComboBox, - ClockTwoTimeZoneComboBox, - ClockThreeTimeZoneComboBox, - ClockFourTimeZoneComboBox - ]; - - LoadState(); - ApplyLocalization(); - PopulateTimeZoneComboBoxes(); - } - - public void SetComponentPlacementContext(string componentId, string? placementId) - { - _componentId = string.IsNullOrWhiteSpace(componentId) - ? BuiltInComponentIds.DesktopWorldClock - : componentId.Trim(); - _placementId = placementId?.Trim() ?? string.Empty; - LoadState(); - ApplyLocalization(); - PopulateTimeZoneComboBoxes(); - } - - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - LoadState(); - ApplyLocalization(); - PopulateTimeZoneComboBoxes(); - } - - private void LoadState() - { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); - _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); - - _allTimeZones = _timeZoneService - .GetAllTimeZones() - .OrderBy(zone => zone.GetUtcOffset(DateTime.UtcNow)) - .ThenBy(zone => zone.DisplayName, StringComparer.OrdinalIgnoreCase) - .ToList(); - - _selectedTimeZoneIds = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds( - componentSnapshot.WorldClockTimeZoneIds, - _allTimeZones); - _secondHandMode = ClockSecondHandMode.Normalize(componentSnapshot.WorldClockSecondHandMode); - } - - private void ApplyLocalization() - { - TitleTextBlock.Text = L("worldclock.settings.title", "世界时钟设置"); - DescriptionTextBlock.Text = L("worldclock.settings.desc", "分别为四个时钟选择时区。"); - - ClockOneLabelTextBlock.Text = L("worldclock.settings.clock_1", "时钟 1"); - ClockTwoLabelTextBlock.Text = L("worldclock.settings.clock_2", "时钟 2"); - ClockThreeLabelTextBlock.Text = L("worldclock.settings.clock_3", "时钟 3"); - ClockFourLabelTextBlock.Text = L("worldclock.settings.clock_4", "时钟 4"); - SecondHandModeLabelTextBlock.Text = L("worldclock.settings.second_mode_label", "秒针方式"); - SecondHandTickRadioButton.Content = L("clock.second_mode.tick", "跳针"); - SecondHandSweepRadioButton.Content = L("clock.second_mode.sweep", "扫针"); - } - - private void PopulateTimeZoneComboBoxes() - { - _suppressEvents = true; - try - { - foreach (var comboBox in _timeZoneComboBoxes) - { - comboBox.Items.Clear(); - foreach (var timeZone in _allTimeZones) - { - comboBox.Items.Add(new ComboBoxItem - { - Tag = timeZone.Id, - Content = GetLocalizedTimeZoneDisplayName(timeZone) - }); - } - } - - for (var index = 0; index < _timeZoneComboBoxes.Length; index++) - { - var comboBox = _timeZoneComboBoxes[index]; - var targetId = index < _selectedTimeZoneIds.Count - ? _selectedTimeZoneIds[index] - : TimeZoneInfo.Local.Id; - - var selected = comboBox.Items - .OfType() - .FirstOrDefault(item => string.Equals(item.Tag as string, targetId, StringComparison.OrdinalIgnoreCase)); - - comboBox.SelectedItem = selected ?? comboBox.Items.OfType().FirstOrDefault(); - } - - var normalizedMode = ClockSecondHandMode.Normalize(_secondHandMode); - SecondHandTickRadioButton.IsChecked = string.Equals( - normalizedMode, - ClockSecondHandMode.Tick, - StringComparison.OrdinalIgnoreCase); - SecondHandSweepRadioButton.IsChecked = string.Equals( - normalizedMode, - ClockSecondHandMode.Sweep, - StringComparison.OrdinalIgnoreCase); - } - finally - { - _suppressEvents = false; - } - } - - private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void OnSecondHandModeChanged(object? sender, RoutedEventArgs e) - { - _ = sender; - _ = e; - if (_suppressEvents) - { - return; - } - - SaveState(); - } - - private void SaveState() - { - var selectedIds = GetSelectedTimeZoneIds(); - var normalizedIds = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(selectedIds, _allTimeZones); - _secondHandMode = GetSelectedSecondHandMode(); - - var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); - snapshot.WorldClockTimeZoneIds = normalizedIds.ToList(); - snapshot.WorldClockSecondHandMode = _secondHandMode; - _componentSettingsStore.SaveForComponent(_componentId, _placementId, snapshot); - - _selectedTimeZoneIds = normalizedIds; - SettingsChanged?.Invoke(this, EventArgs.Empty); - } - - private string GetSelectedSecondHandMode() - { - return SecondHandSweepRadioButton.IsChecked == true - ? ClockSecondHandMode.Sweep - : ClockSecondHandMode.Tick; - } - - private List GetSelectedTimeZoneIds() - { - var selectedIds = new List(_timeZoneComboBoxes.Length); - foreach (var comboBox in _timeZoneComboBoxes) - { - if (comboBox.SelectedItem is ComboBoxItem item && - item.Tag is string timeZoneId && - !string.IsNullOrWhiteSpace(timeZoneId)) - { - selectedIds.Add(timeZoneId.Trim()); - continue; - } - - selectedIds.Add(TimeZoneInfo.Local.Id); - } - - return selectedIds; - } - - private string GetLocalizedTimeZoneDisplayName(TimeZoneInfo timeZone) - { - var offset = timeZone.GetUtcOffset(DateTime.UtcNow); - var sign = offset >= TimeSpan.Zero ? "+" : "-"; - var totalMinutes = Math.Abs((int)offset.TotalMinutes); - var hours = totalMinutes / 60; - var minutes = totalMinutes % 60; - - var displayName = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) - ? ResolveZhDisplayName(timeZone) - : ResolveEnDisplayName(timeZone); - - return $"(UTC{sign}{hours:D2}:{minutes:D2}) {displayName}"; - } - - private static string ResolveZhDisplayName(TimeZoneInfo timeZone) - { - if (ZhTimeZoneNames.TryGetValue(timeZone.Id, out var localizedName)) - { - return localizedName; - } - - return string.IsNullOrWhiteSpace(timeZone.StandardName) - ? timeZone.DisplayName - : timeZone.StandardName; - } - - private static string ResolveEnDisplayName(TimeZoneInfo timeZone) - { - if (!string.IsNullOrWhiteSpace(timeZone.StandardName)) - { - return timeZone.StandardName; - } - - return timeZone.DisplayName; - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } -} diff --git a/LanMountainDesktop/Views/IndependentSettingsModuleWindowBase.cs b/LanMountainDesktop/Views/IndependentSettingsModuleWindowBase.cs deleted file mode 100644 index af3955b..0000000 --- a/LanMountainDesktop/Views/IndependentSettingsModuleWindowBase.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Media; -using FluentAvalonia.UI.Windowing; - -namespace LanMountainDesktop.Views; - -public class IndependentSettingsModuleWindowBase : AppWindow -{ - public IndependentSettingsModuleWindowBase() - { - TitleBar.ExtendsContentIntoTitleBar = true; - TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex; - TitleBar.Height = 48; - - if (OperatingSystem.IsWindows()) - { - TransparencyLevelHint = [WindowTransparencyLevel.Mica]; - Background = Brushes.Transparent; - } - } -} diff --git a/LanMountainDesktop/Views/IndependentSettingsPageCategory.cs b/LanMountainDesktop/Views/IndependentSettingsPageCategory.cs deleted file mode 100644 index bfb126b..0000000 --- a/LanMountainDesktop/Views/IndependentSettingsPageCategory.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace LanMountainDesktop.Views; - -internal enum IndependentSettingsPageCategory -{ - Internal = 0, - External = 1, - About = 2, - Debug = 3 -} diff --git a/LanMountainDesktop/Views/IndependentSettingsPageDefinition.cs b/LanMountainDesktop/Views/IndependentSettingsPageDefinition.cs deleted file mode 100644 index c8401cd..0000000 --- a/LanMountainDesktop/Views/IndependentSettingsPageDefinition.cs +++ /dev/null @@ -1,12 +0,0 @@ -using FluentIcons.Common; - -namespace LanMountainDesktop.Views; - -internal sealed record IndependentSettingsPageDefinition( - string Tag, - string Title, - string Description, - Symbol Icon, - IndependentSettingsPageCategory Category, - int SortOrder, - string? ToolTip = null); diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs index e7134d8..1dbce4b 100644 --- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs +++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs @@ -108,23 +108,18 @@ public partial class MainWindow return; } - _reopenSettingsAfterComponentLibraryClose = _isSettingsOpen; - if (_isSettingsOpen) - { - CloseSettingsPage(immediate: true); - } - OpenComponentLibraryWindow(); } private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e) { - CloseComponentLibraryWindow(reopenSettings: true); + CloseComponentLibraryWindow(reopenSettings: false); } private void OnCloseComponentSettingsClick(object? sender, RoutedEventArgs e) { - CloseComponentSettingsWindow(); + _ = sender; + _ = e; } private void OnStatusBarClockChecked(object? sender, RoutedEventArgs e) @@ -251,29 +246,13 @@ public partial class MainWindow private TaskbarContext GetCurrentTaskbarContext() { - if (!_isSettingsOpen) - { - return TaskbarContext.Desktop; - } - - var selectedItem = SettingsNavView?.SelectedItem as FluentAvalonia.UI.Controls.NavigationViewItem; - return selectedItem?.Tag?.ToString() switch - { - "Wallpaper" => TaskbarContext.SettingsWallpaper, - "Grid" => TaskbarContext.SettingsGrid, - "Color" => TaskbarContext.SettingsColor, - "StatusBar" => TaskbarContext.SettingsStatusBar, - "Weather" => TaskbarContext.SettingsWeather, - "Region" => TaskbarContext.SettingsRegion, - _ => TaskbarContext.Desktop - }; + return TaskbarContext.Desktop; } private void ApplyTaskbarActionVisibility(TaskbarContext context) { if (BackToWindowsButton is null || OpenComponentLibraryButton is null || - OpenSettingsButton is null || WallpaperPreviewBackButtonVisual is null || WallpaperPreviewComponentLibraryVisual is null || WallpaperPreviewSettingsButtonIcon is null) @@ -282,12 +261,11 @@ public partial class MainWindow } var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows); - var showSettings = _pinnedTaskbarActions.Contains(TaskbarActionId.OpenSettings); - var showDesktopEdit = _isSettingsOpen; + var showSettings = false; + var showDesktopEdit = true; BackToWindowsButton.IsVisible = showMinimize; OpenComponentLibraryButton.IsVisible = showDesktopEdit; - OpenSettingsButton.IsVisible = showSettings; WallpaperPreviewBackButtonVisual.IsVisible = showMinimize; WallpaperPreviewComponentLibraryVisual.IsVisible = showDesktopEdit; WallpaperPreviewSettingsButtonIcon.IsVisible = showSettings; @@ -327,30 +305,11 @@ public partial class MainWindow { WallpaperPreviewTaskbarDynamicActionsHost.IsVisible = hasDynamicActions; } - - UpdateOpenSettingsActionVisualState(); } private void UpdateOpenSettingsActionVisualState() { - if (OpenSettingsButtonTextBlock is null || OpenSettingsButton is null) - { - return; - } - - var showBackToDesktop = _isSettingsOpen; - OpenSettingsButtonTextBlock.IsVisible = showBackToDesktop; - OpenSettingsButtonTextBlock.Text = L("settings.back_to_desktop", "Back to Desktop"); - ToolTip.SetTip( - OpenSettingsButton, - showBackToDesktop - ? L("settings.back_to_desktop", "Back to Desktop") - : 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); + // Open-settings action is removed in API-only settings mode. } private void OpenComponentLibraryWindow() @@ -410,10 +369,9 @@ public partial class MainWindow _reopenSettingsAfterComponentLibraryClose = false; if (shouldReopenSettings) { - if (Application.Current is App app) - { - app.OpenIndependentSettingsModule("ComponentLibrary"); - } + AppLogger.Info( + "SettingsFacade", + "Reopen settings request ignored because settings UI entry is disabled during hard-cut migration."); } }, FluttermotionToken.Slow); } @@ -452,13 +410,6 @@ public partial class MainWindow IsVisible: true, CommandKey: "component.delete")); - actions.Add(new TaskbarActionItem( - TaskbarActionId.EditComponent, - L("component.edit", "Edit"), - "Edit", - IsVisible: true, - CommandKey: "component.edit")); - return actions; } @@ -556,17 +507,12 @@ public partial class MainWindow var isDeleteAction = action.Id == TaskbarActionId.DeleteDesktopPage || action.Id == TaskbarActionId.DeleteComponent; var isHideAction = action.Id == TaskbarActionId.HideLauncherEntry; - var isEditAction = action.Id == TaskbarActionId.EditComponent; Symbol iconSymbol; if (isDeleteAction || isHideAction) { iconSymbol = Symbol.Delete; } - else if (isEditAction) - { - iconSymbol = Symbol.Edit; - } else { iconSymbol = Symbol.Add; @@ -662,9 +608,6 @@ public partial class MainWindow case "component.delete": DeleteSelectedComponent(); break; - case "component.edit": - OpenComponentSettings(); - break; case "launcher.hide": HideSelectedLauncherEntry(); break; @@ -714,583 +657,7 @@ public partial class MainWindow } } - private void OpenComponentSettings() - { - if (_selectedDesktopComponentHost is null || _selectedDesktopComponentHost.Tag is not string placementId) - { - return; - } - - var placement = _desktopComponentPlacements.FirstOrDefault(p => - string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase)); - if (placement is null) - { - return; - } - - if (placement.ComponentId == BuiltInComponentIds.Date) - { - OpenDateComponentSettings(placement); - return; - } - - if (placement.ComponentId == BuiltInComponentIds.DesktopClock) - { - OpenDesktopClockComponentSettings(placement); - return; - } - - if (placement.ComponentId == BuiltInComponentIds.DesktopClassSchedule) - { - OpenClassScheduleComponentSettings(placement); - return; - } - - if (placement.ComponentId == BuiltInComponentIds.DesktopWorldClock) - { - OpenWorldClockComponentSettings(placement); - return; - } - - if (IsWeatherComponentId(placement.ComponentId)) - { - OpenWeatherComponentSettings(placement); - return; - } - - if (placement.ComponentId == BuiltInComponentIds.DesktopDailyArtwork) - { - OpenDailyArtworkComponentSettings(placement); - return; - } - - if (placement.ComponentId == BuiltInComponentIds.DesktopCnrDailyNews) - { - OpenCnrDailyNewsComponentSettings(placement); - return; - } - - if (placement.ComponentId == BuiltInComponentIds.DesktopIfengNews) - { - OpenIfengNewsComponentSettings(placement); - return; - } - - if (placement.ComponentId == BuiltInComponentIds.DesktopDailyWord || - placement.ComponentId == BuiltInComponentIds.DesktopDailyWord2x2) - { - OpenDailyWordComponentSettings(placement); - return; - } - - if (placement.ComponentId == BuiltInComponentIds.DesktopBilibiliHotSearch) - { - OpenBilibiliHotSearchComponentSettings(placement); - return; - } - - if (placement.ComponentId == BuiltInComponentIds.DesktopBaiduHotSearch) - { - OpenBaiduHotSearchComponentSettings(placement); - return; - } - - if (placement.ComponentId == BuiltInComponentIds.DesktopStcn24Forum) - { - OpenStcn24ForumComponentSettings(placement); - return; - } - - if (placement.ComponentId == BuiltInComponentIds.DesktopStudyEnvironment) - { - OpenStudyEnvironmentComponentSettings(placement); - return; - } - } - - private static bool IsWeatherComponentId(string componentId) - { - return string.Equals(componentId, BuiltInComponentIds.DesktopWeather, StringComparison.OrdinalIgnoreCase) || - string.Equals(componentId, BuiltInComponentIds.DesktopWeatherClock, StringComparison.OrdinalIgnoreCase) || - string.Equals(componentId, BuiltInComponentIds.DesktopHourlyWeather, StringComparison.OrdinalIgnoreCase) || - string.Equals(componentId, BuiltInComponentIds.DesktopMultiDayWeather, StringComparison.OrdinalIgnoreCase) || - string.Equals(componentId, BuiltInComponentIds.DesktopExtendedWeather, StringComparison.OrdinalIgnoreCase); - } - - private void ShowComponentSettings(Control settingsContent, DesktopComponentPlacementSnapshot placement) - { - if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null) - { - return; - } - - var runtimeContext = new DesktopComponentRuntimeContext( - placement.ComponentId, - placement.PlacementId, - _componentSettingsService); - - if (settingsContent is IComponentRuntimeContextAware runtimeContextAwareComponent) - { - runtimeContextAwareComponent.SetComponentRuntimeContext(runtimeContext); - } - - if (settingsContent is IComponentPlacementContextAware placementAwareComponent) - { - placementAwareComponent.SetComponentPlacementContext(placement.ComponentId, placement.PlacementId); - } - - if (settingsContent is IComponentSettingsStoreAware settingsStoreAwareComponent) - { - settingsStoreAwareComponent.SetComponentSettingsStore(_componentSettingsService); - } - - ComponentSettingsService.ApplyScopedContextToTarget(settingsContent, placement.ComponentId, placement.PlacementId); - - ComponentSettingsContentHost.Content = settingsContent; - ComponentSettingsWindow.IsVisible = true; - ComponentSettingsWindow.Opacity = 0; - ComponentSettingsWindow.Opacity = 1; - } - - private void OpenDateComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new DateWidgetSettingsWindow(); - ShowComponentSettings(settingsContent, placement); - } - - private void OpenClassScheduleComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new ClassScheduleSettingsWindow(); - settingsContent.SettingsChanged += OnClassScheduleSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OpenDesktopClockComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new AnalogClockWidgetSettingsWindow(); - settingsContent.SettingsChanged += OnDesktopClockSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OpenWorldClockComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new WorldClockWidgetSettingsWindow(); - settingsContent.SettingsChanged += OnWorldClockSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OpenWeatherComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new WeatherWidgetSettingsWindow(); - settingsContent.SettingsChanged += OnWeatherSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OpenStudyEnvironmentComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new StudyEnvironmentWidgetSettingsWindow(); - settingsContent.SettingsChanged += OnStudyEnvironmentSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OpenDailyArtworkComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new DailyArtworkSettingsWindow(); - settingsContent.SettingsChanged += OnDailyArtworkSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OpenCnrDailyNewsComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new CnrDailyNewsSettingsWindow(); - settingsContent.SettingsChanged += OnCnrDailyNewsSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OpenIfengNewsComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new IfengNewsSettingsWindow(); - settingsContent.SettingsChanged += OnIfengNewsSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OpenDailyWordComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new DailyWordSettingsWindow(); - settingsContent.SettingsChanged += OnDailyWordSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OpenBilibiliHotSearchComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new BilibiliHotSearchSettingsWindow(); - settingsContent.SettingsChanged += OnBilibiliHotSearchSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OpenBaiduHotSearchComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new BaiduHotSearchSettingsWindow(); - settingsContent.SettingsChanged += OnBaiduHotSearchSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OpenStcn24ForumComponentSettings(DesktopComponentPlacementSnapshot placement) - { - var settingsContent = new Stcn24ForumSettingsWindow(); - settingsContent.SettingsChanged += OnStcn24ForumSettingsChanged; - ShowComponentSettings(settingsContent, placement); - } - - private void OnClassScheduleSettingsChanged(object? sender, EventArgs e) - { - if (_selectedDesktopComponentHost is null) - { - return; - } - - if (TryGetContentHost(_selectedDesktopComponentHost)?.Child is ClassScheduleWidget widget) - { - widget.RefreshFromSettings(); - } - } - - private void OnDesktopClockSettingsChanged(object? sender, EventArgs e) - { - _ = sender; - _ = e; - - foreach (var pageGrid in _desktopPageComponentGrids.Values) - { - foreach (var host in pageGrid.Children.OfType()) - { - if (!host.Classes.Contains(DesktopComponentHostClass)) - { - continue; - } - - if (TryGetContentHost(host)?.Child is AnalogClockWidget widget) - { - widget.RefreshFromSettings(); - } - } - } - } - - private void OnStudyEnvironmentSettingsChanged(object? sender, EventArgs e) - { - _ = sender; - _ = e; - if (_selectedDesktopComponentHost is null) - { - return; - } - - if (TryGetContentHost(_selectedDesktopComponentHost)?.Child is StudyEnvironmentWidget widget) - { - widget.RefreshFromSettings(); - } - } - - private void OnDailyArtworkSettingsChanged(object? sender, EventArgs e) - { - _ = sender; - _ = e; - - foreach (var pageGrid in _desktopPageComponentGrids.Values) - { - foreach (var host in pageGrid.Children.OfType()) - { - if (!host.Classes.Contains(DesktopComponentHostClass)) - { - continue; - } - - if (TryGetContentHost(host)?.Child is DailyArtworkWidget widget) - { - widget.RefreshFromSettings(); - } - } - } - } - - private void OnWorldClockSettingsChanged(object? sender, EventArgs e) - { - _ = sender; - _ = e; - - foreach (var pageGrid in _desktopPageComponentGrids.Values) - { - foreach (var host in pageGrid.Children.OfType()) - { - if (!host.Classes.Contains(DesktopComponentHostClass)) - { - continue; - } - - if (TryGetContentHost(host)?.Child is WorldClockWidget widget) - { - widget.RefreshFromSettings(); - } - } - } - } - - private void OnWeatherSettingsChanged(object? sender, EventArgs e) - { - _ = sender; - _ = e; - - foreach (var pageGrid in _desktopPageComponentGrids.Values) - { - foreach (var host in pageGrid.Children.OfType()) - { - if (!host.Classes.Contains(DesktopComponentHostClass)) - { - continue; - } - - var child = TryGetContentHost(host)?.Child; - switch (child) - { - case WeatherWidget weatherWidget: - weatherWidget.RefreshFromSettings(); - break; - case WeatherClockWidget weatherClockWidget: - weatherClockWidget.RefreshFromSettings(); - break; - case HourlyWeatherWidget hourlyWeatherWidget: - hourlyWeatherWidget.RefreshFromSettings(); - break; - case MultiDayWeatherWidget multiDayWeatherWidget: - multiDayWeatherWidget.RefreshFromSettings(); - break; - case ExtendedWeatherWidget extendedWeatherWidget: - extendedWeatherWidget.RefreshFromSettings(); - break; - } - } - } - } - - private void OnCnrDailyNewsSettingsChanged(object? sender, EventArgs e) - { - _ = sender; - _ = e; - - foreach (var pageGrid in _desktopPageComponentGrids.Values) - { - foreach (var host in pageGrid.Children.OfType()) - { - if (!host.Classes.Contains(DesktopComponentHostClass)) - { - continue; - } - - if (TryGetContentHost(host)?.Child is CnrDailyNewsWidget widget) - { - widget.RefreshFromSettings(); - } - } - } - } - - private void OnIfengNewsSettingsChanged(object? sender, EventArgs e) - { - _ = sender; - _ = e; - - foreach (var pageGrid in _desktopPageComponentGrids.Values) - { - foreach (var host in pageGrid.Children.OfType()) - { - if (!host.Classes.Contains(DesktopComponentHostClass)) - { - continue; - } - - if (TryGetContentHost(host)?.Child is IfengNewsWidget widget) - { - widget.RefreshFromSettings(); - } - } - } - } - - private void OnDailyWordSettingsChanged(object? sender, EventArgs e) - { - _ = sender; - _ = e; - - foreach (var pageGrid in _desktopPageComponentGrids.Values) - { - foreach (var host in pageGrid.Children.OfType()) - { - if (!host.Classes.Contains(DesktopComponentHostClass)) - { - continue; - } - - var widget = TryGetContentHost(host)?.Child; - if (widget is DailyWordWidget dailyWordWidget) - { - dailyWordWidget.RefreshFromSettings(); - } - else if (widget is DailyWord2x2Widget dailyWord2x2Widget) - { - dailyWord2x2Widget.RefreshFromSettings(); - } - } - } - } - - private void OnBilibiliHotSearchSettingsChanged(object? sender, EventArgs e) - { - _ = sender; - _ = e; - - foreach (var pageGrid in _desktopPageComponentGrids.Values) - { - foreach (var host in pageGrid.Children.OfType()) - { - if (!host.Classes.Contains(DesktopComponentHostClass)) - { - continue; - } - - if (TryGetContentHost(host)?.Child is BilibiliHotSearchWidget widget) - { - widget.RefreshFromSettings(); - } - } - } - } - - private void OnBaiduHotSearchSettingsChanged(object? sender, EventArgs e) - { - _ = sender; - _ = e; - - foreach (var pageGrid in _desktopPageComponentGrids.Values) - { - foreach (var host in pageGrid.Children.OfType()) - { - if (!host.Classes.Contains(DesktopComponentHostClass)) - { - continue; - } - - if (TryGetContentHost(host)?.Child is BaiduHotSearchWidget widget) - { - widget.RefreshFromSettings(); - } - } - } - } - - private void OnStcn24ForumSettingsChanged(object? sender, EventArgs e) - { - _ = sender; - _ = e; - - foreach (var pageGrid in _desktopPageComponentGrids.Values) - { - foreach (var host in pageGrid.Children.OfType()) - { - if (!host.Classes.Contains(DesktopComponentHostClass)) - { - continue; - } - - if (TryGetContentHost(host)?.Child is Stcn24ForumWidget widget) - { - widget.RefreshFromSettings(); - } - } - } - } - - private void CloseComponentSettingsWindow() - { - if (ComponentSettingsWindow is null) - { - return; - } - - if (ComponentSettingsContentHost?.Content is ClassScheduleSettingsWindow classScheduleSettingsWindow) - { - classScheduleSettingsWindow.SettingsChanged -= OnClassScheduleSettingsChanged; - } - - if (ComponentSettingsContentHost?.Content is AnalogClockWidgetSettingsWindow analogClockSettingsWindow) - { - analogClockSettingsWindow.SettingsChanged -= OnDesktopClockSettingsChanged; - } - - if (ComponentSettingsContentHost?.Content is StudyEnvironmentWidgetSettingsWindow studyEnvironmentSettingsWindow) - { - studyEnvironmentSettingsWindow.SettingsChanged -= OnStudyEnvironmentSettingsChanged; - } - - if (ComponentSettingsContentHost?.Content is DailyArtworkSettingsWindow dailyArtworkSettingsWindow) - { - dailyArtworkSettingsWindow.SettingsChanged -= OnDailyArtworkSettingsChanged; - } - - if (ComponentSettingsContentHost?.Content is WorldClockWidgetSettingsWindow worldClockSettingsWindow) - { - worldClockSettingsWindow.SettingsChanged -= OnWorldClockSettingsChanged; - } - - if (ComponentSettingsContentHost?.Content is WeatherWidgetSettingsWindow weatherSettingsWindow) - { - weatherSettingsWindow.SettingsChanged -= OnWeatherSettingsChanged; - } - - if (ComponentSettingsContentHost?.Content is CnrDailyNewsSettingsWindow cnrDailyNewsSettingsWindow) - { - cnrDailyNewsSettingsWindow.SettingsChanged -= OnCnrDailyNewsSettingsChanged; - } - - if (ComponentSettingsContentHost?.Content is IfengNewsSettingsWindow ifengNewsSettingsWindow) - { - ifengNewsSettingsWindow.SettingsChanged -= OnIfengNewsSettingsChanged; - } - - if (ComponentSettingsContentHost?.Content is DailyWordSettingsWindow dailyWordSettingsWindow) - { - dailyWordSettingsWindow.SettingsChanged -= OnDailyWordSettingsChanged; - } - - if (ComponentSettingsContentHost?.Content is BilibiliHotSearchSettingsWindow bilibiliHotSearchSettingsWindow) - { - bilibiliHotSearchSettingsWindow.SettingsChanged -= OnBilibiliHotSearchSettingsChanged; - } - - if (ComponentSettingsContentHost?.Content is BaiduHotSearchSettingsWindow baiduHotSearchSettingsWindow) - { - baiduHotSearchSettingsWindow.SettingsChanged -= OnBaiduHotSearchSettingsChanged; - } - - if (ComponentSettingsContentHost?.Content is Stcn24ForumSettingsWindow stcn24ForumSettingsWindow) - { - stcn24ForumSettingsWindow.SettingsChanged -= OnStcn24ForumSettingsChanged; - } - - ComponentSettingsWindow.Opacity = 0; - - DispatcherTimer.RunOnce(() => - { - if (ComponentSettingsWindow is not null) - { - ComponentSettingsWindow.IsVisible = false; - } - if (ComponentSettingsContentHost is not null) - { - ComponentSettingsContentHost.Content = null; - } - }, FluttermotionToken.Slow); - } + // Component settings popup UI is removed in API-only settings hard-cut mode. private void AddDesktopPage() { diff --git a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs index 671cbec..8f59c3e 100644 --- a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs +++ b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs @@ -147,7 +147,7 @@ public partial class MainWindow } } - private void UpdateDesktopSurfaceLayout(GridMetrics gridMetrics) + private void UpdateDesktopSurfaceLayout(DesktopGridMetrics gridMetrics) { if (DesktopPagesViewport is null || DesktopPagesHost is null || diff --git a/LanMountainDesktop/Views/MainWindow.Settings.cs b/LanMountainDesktop/Views/MainWindow.Settings.cs index 103f7ad..e813780 100644 --- a/LanMountainDesktop/Views/MainWindow.Settings.cs +++ b/LanMountainDesktop/Views/MainWindow.Settings.cs @@ -83,10 +83,7 @@ public partial class MainWindow CloseSettingsPage(immediate: true); } - if (Application.Current is App app) - { - app.OpenIndependentSettingsModule("MainWindow"); - } + AppLogger.Info("SettingsFacade", "Open settings entry is disabled during hard-cut settings API migration."); } private void OnCloseSettingsClick(object? sender, RoutedEventArgs e) @@ -1009,7 +1006,7 @@ public partial class MainWindow MinShortSideCells, MaxShortSideCells); - _gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset); + _gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset(snapshot.GridSpacingPreset); _suppressGridSpacingEvents = true; GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0; diff --git a/LanMountainDesktop/Views/MainWindow.axaml b/LanMountainDesktop/Views/MainWindow.axaml index efb65bc..4fb897b 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml +++ b/LanMountainDesktop/Views/MainWindow.axaml @@ -312,29 +312,6 @@ - @@ -521,47 +498,6 @@ - - - - - - - - - - - - CellSize + GapPx; - } + private readonly DesktopGridLayoutService _gridLayoutService = new(); private readonly MonetColorService _monetColorService = new(); private readonly AppSettingsService _appSettingsService = new(); private readonly DesktopLayoutSettingsService _desktopLayoutSettingsService = new(); @@ -300,7 +289,7 @@ public partial class MainWindow : Window MinShortSideCells, MaxShortSideCells); - _gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset); + _gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset(snapshot.GridSpacingPreset); _suppressGridSpacingEvents = true; GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0; _suppressGridSpacingEvents = false; @@ -637,11 +626,11 @@ public partial class MainWindow : Window var innerWidth = Math.Max(1, gridPreviewWidth - horizontalPadding); var innerHeight = Math.Max(1, gridPreviewHeight - verticalPadding); - var preset = NormalizeGridSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset); - var gapRatio = ResolveGridGapRatio(preset); + var preset = _gridLayoutService.NormalizeSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset); + var gapRatio = _gridLayoutService.ResolveGapRatio(preset); var pendingEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent(); - var edgeInset = CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent); - var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset); + var edgeInset = _gridLayoutService.CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent); + var gridMetrics = _gridLayoutService.CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset); if (gridMetrics.CellSize <= 0) { return; @@ -697,7 +686,7 @@ public partial class MainWindow : Window DrawGridPreviewLines(gridMetrics); } - private void DrawGridPreviewLines(GridMetrics gridMetrics) + private void DrawGridPreviewLines(DesktopGridMetrics gridMetrics) { if (GridPreviewLinesCanvas is null || GridPreviewViewport is null || GridPreviewGrid is null) { @@ -778,7 +767,7 @@ public partial class MainWindow : Window private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e) { - _gridSpacingPreset = NormalizeGridSpacingPreset( + _gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset( TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset); _desktopEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent(); @@ -836,9 +825,9 @@ public partial class MainWindow : Window { var hostWidth = DesktopHost.Bounds.Width; var hostHeight = DesktopHost.Bounds.Height; - var gapRatio = ResolveGridGapRatio(_gridSpacingPreset); - var edgeInset = CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent); - var gridMetrics = CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset); + var gapRatio = _gridLayoutService.ResolveGapRatio(_gridSpacingPreset); + var edgeInset = _gridLayoutService.CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent); + var gridMetrics = _gridLayoutService.CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset); if (gridMetrics.CellSize <= 0) { return; @@ -960,13 +949,6 @@ public partial class MainWindow : Window insetPx); } - private static string NormalizeGridSpacingPreset(string? value) - { - return string.Equals(value, "Compact", StringComparison.OrdinalIgnoreCase) - ? "Compact" - : "Relaxed"; - } - private static string NormalizeStatusBarSpacingMode(string? value) { return value switch @@ -987,99 +969,6 @@ public partial class MainWindow : Window return comboBox?.SelectedItem?.ToString(); } - private static double ResolveGridGapRatio(string preset) - { - return string.Equals(preset, "Compact", StringComparison.OrdinalIgnoreCase) ? 0.06 : 0.12; - } - - private static double CalculateEdgeInset(double hostWidth, double hostHeight, int shortSideCells, int insetPercent) - { - if (hostWidth <= 1 || hostHeight <= 1) - { - return 0; - } - - var cells = Math.Max(1, shortSideCells); - var shortSidePx = Math.Max(1, Math.Min(hostWidth, hostHeight)); - var baseCell = shortSidePx / cells; - - // Proportional inset based on user percentage selection. - var clampedPercent = Math.Clamp(insetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent); - var insetRatio = clampedPercent / 100d; - - // Keep inset within a practical visual range. - return Math.Clamp(baseCell * insetRatio, 0, 80); - } - - private static GridMetrics CalculateGridMetrics( - double hostWidth, - double hostHeight, - int shortSideCells, - double gapRatio, - double edgeInsetPx) - { - if (hostWidth <= 1 || hostHeight <= 1) - { - return default; - } - - var shortSide = Math.Max(1, shortSideCells); - var clampedGapRatio = Math.Max(0, gapRatio); - var inset = Math.Max(0, edgeInsetPx); - - // Edge inset should come only from user setting. - // Remaining free space is handled by container centering, not baked into inset. - var availableWidth = Math.Max(1, hostWidth - inset * 2); - var availableHeight = Math.Max(1, hostHeight - inset * 2); - - if (hostWidth >= hostHeight) - { - var rowCount = shortSide; - var denominator = rowCount + Math.Max(0, rowCount - 1) * clampedGapRatio; - if (denominator <= 0) - { - return default; - } - - var cellSize = availableHeight / denominator; - var gapPx = cellSize * clampedGapRatio; - var pitch = cellSize + gapPx; - if (pitch <= 0) - { - return default; - } - - var columnCount = Math.Max(1, (int)Math.Floor((availableWidth + gapPx) / pitch)); - var gridWidth = columnCount * cellSize + Math.Max(0, columnCount - 1) * gapPx; - var gridHeight = rowCount * cellSize + Math.Max(0, rowCount - 1) * gapPx; - - return new GridMetrics(columnCount, rowCount, cellSize, gapPx, inset, gridWidth, gridHeight); - } - else - { - var columnCount = shortSide; - var denominator = columnCount + Math.Max(0, columnCount - 1) * clampedGapRatio; - if (denominator <= 0) - { - return default; - } - - var cellSize = availableWidth / denominator; - var gapPx = cellSize * clampedGapRatio; - var pitch = cellSize + gapPx; - if (pitch <= 0) - { - return default; - } - - var rowCount = Math.Max(1, (int)Math.Floor((availableHeight + gapPx) / pitch)); - var gridWidth = columnCount * cellSize + Math.Max(0, columnCount - 1) * gapPx; - var gridHeight = rowCount * cellSize + Math.Max(0, rowCount - 1) * gapPx; - - return new GridMetrics(columnCount, rowCount, cellSize, gapPx, inset, gridWidth, gridHeight); - } - } - private static int ClampComponentSpan(int requestedSpan, int axisCellCount) { return Math.Clamp(requestedSpan, 1, Math.Max(1, axisCellCount)); @@ -1110,7 +999,6 @@ public partial class MainWindow : Window var taskbarTextSize = Math.Clamp(taskbarCellHeight * 0.36, 12, 22); var taskbarIconSize = Math.Clamp(taskbarCellHeight * 0.46, 16, 34); var taskbarButtonInset = Math.Clamp(taskbarCellHeight * 0.22, 6, 16); - var compactButtonInset = Math.Clamp(taskbarCellHeight * 0.20, 6, 14); var buttonContentSpacing = Math.Clamp(taskbarCellHeight * 0.20, 6, 14); var taskbarButtonPadding = new Thickness(taskbarButtonInset); @@ -1145,27 +1033,6 @@ public partial class MainWindow : Window OpenComponentLibraryTextBlock.FontSize = taskbarTextSize; SetButtonContentSpacing(OpenComponentLibraryButton, buttonContentSpacing); - OpenSettingsButton.Margin = new Thickness(0); - OpenSettingsButton.Height = taskbarCellHeight; - OpenSettingsButton.MinHeight = taskbarCellHeight; - OpenSettingsButton.FontSize = taskbarTextSize; - OpenSettingsButtonTextBlock.FontSize = taskbarTextSize; - OpenSettingsIcon.FontSize = taskbarIconSize; - SetButtonContentSpacing(OpenSettingsButton, Math.Clamp(taskbarCellHeight * 0.18, 4, 10)); - - if (_isSettingsOpen) - { - OpenSettingsButton.Width = double.NaN; - OpenSettingsButton.MinWidth = Math.Clamp(taskbarCellHeight * 2.45, 120, 360); - OpenSettingsButton.Padding = taskbarButtonPadding; - } - else - { - OpenSettingsButton.Width = taskbarCellHeight; - OpenSettingsButton.MinWidth = taskbarCellHeight; - OpenSettingsButton.Padding = new Thickness(compactButtonInset); - } - UpdateComponentLibraryLayout(cellSize); } @@ -1293,9 +1160,9 @@ public partial class MainWindow : Window var innerWidth = Math.Max(1, previewWidth - horizontalPadding); var innerHeight = Math.Max(1, previewHeight - verticalPadding); - var gapRatio = ResolveGridGapRatio(_gridSpacingPreset); - var edgeInset = CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent); - var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset); + var gapRatio = _gridLayoutService.ResolveGapRatio(_gridSpacingPreset); + var edgeInset = _gridLayoutService.CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent); + var gridMetrics = _gridLayoutService.CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset); if (gridMetrics.CellSize <= 0) { return; diff --git a/LanMountainDesktop/Views/SettingsWindow.ComponentsSummary.cs b/LanMountainDesktop/Views/SettingsWindow.ComponentsSummary.cs deleted file mode 100644 index 952f9ee..0000000 --- a/LanMountainDesktop/Views/SettingsWindow.ComponentsSummary.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Linq; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Layout; -using Avalonia.Media; - -namespace LanMountainDesktop.Views; - -public partial class SettingsWindow -{ - private void UpdateComponentsSettingsSummary() - { - if (ComponentsSettingsHubPanel is null) - { - return; - } - - var definitions = _componentRegistry - .GetAll() - .OrderBy(definition => definition.Category, StringComparer.OrdinalIgnoreCase) - .ThenBy(definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase) - .ToList(); - - var runtime = (Application.Current as App)?.PluginRuntimeService; - var pluginComponentIds = runtime?.DesktopComponents - .Select(contribution => contribution.Registration.ComponentId) - .ToHashSet(StringComparer.OrdinalIgnoreCase) ?? []; - - var pluginCount = definitions.Count(definition => pluginComponentIds.Contains(definition.Id)); - var builtInCount = definitions.Count - pluginCount; - var desktopCount = definitions.Count(definition => definition.AllowDesktopPlacement); - var statusBarCount = definitions.Count(definition => definition.AllowStatusBarPlacement); - - ComponentsSettingsHubPanel.ComponentsSummaryTextBlock.Text = Lf( - "settings.components.summary_format", - "Available components: {0}. Built-in: {1}. Plugin-provided: {2}. Desktop: {3}. Status bar: {4}.", - definitions.Count, - builtInCount, - pluginCount, - desktopCount, - statusBarCount); - - ComponentsSettingsHubPanel.ComponentCategoryItemsPanel.Children.Clear(); - foreach (var group in definitions - .GroupBy(definition => definition.Category, StringComparer.OrdinalIgnoreCase) - .OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)) - { - ComponentsSettingsHubPanel.ComponentCategoryItemsPanel.Children.Add(new Border - { - Margin = new Thickness(0, 4, 0, 0), - Padding = new Thickness(12, 10), - Background = GetThemeBrush("LayerFillColorDefaultBrush"), - BorderBrush = GetThemeBrush("CardStrokeColorDefaultBrush"), - BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(12), - Child = new StackPanel - { - Orientation = Orientation.Horizontal, - Spacing = 8, - Children = - { - new TextBlock - { - FontWeight = FontWeight.SemiBold, - Text = group.Key - }, - new TextBlock - { - Foreground = GetThemeBrush("TextFillColorSecondaryBrush"), - Text = Lf("settings.components.category_count_format", "{0} item(s)", group.Count()) - } - } - } - }); - } - } -} diff --git a/LanMountainDesktop/Views/SettingsWindow.Controls.cs b/LanMountainDesktop/Views/SettingsWindow.Controls.cs deleted file mode 100644 index e7f5d4d..0000000 --- a/LanMountainDesktop/Views/SettingsWindow.Controls.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Media; -using Avalonia.Media.Imaging; -using LanMountainDesktop.Views.Components; -using LanMountainDesktop.Views.SettingsPages; - -namespace LanMountainDesktop.Views; - -public partial class SettingsWindow -{ - private string CurrentControlAccessStage => _controlsBound ? "module init" : "control binding"; - - private T? TryGetOptionalPageControl(Control? pageRoot, string controlName) - where T : Control - { - return pageRoot?.FindControl(controlName); - } - - private T RequirePageControl(Control pageRoot, string controlName) - where T : Control - { - var control = pageRoot.FindControl(controlName); - if (control is null) - { - throw new InvalidOperationException( - $"Independent settings module control resolution failed. Page='{pageRoot.Name ?? pageRoot.GetType().Name}'; Control='{controlName}'; Stage='{CurrentControlAccessStage}'."); - } - - return control; - } - - private T RequireSettingsPage(T? page, string pageName) - where T : Control - { - return page ?? throw new InvalidOperationException( - $"Independent settings module page resolution failed. Page='{pageName}'; Stage='{CurrentControlAccessStage}'."); - } - - private WallpaperSettingsPage WallpaperSettingsPageRoot => RequireSettingsPage(WallpaperSettingsPanel, nameof(WallpaperSettingsPanel)); - private GridSettingsPage GridSettingsPageRoot => RequireSettingsPage(GridSettingsPanel, nameof(GridSettingsPanel)); - private ColorSettingsPage ColorSettingsPageRoot => RequireSettingsPage(ColorSettingsPanel, nameof(ColorSettingsPanel)); - private StatusBarSettingsPage StatusBarSettingsPageRoot => RequireSettingsPage(StatusBarSettingsPanel, nameof(StatusBarSettingsPanel)); - private RegionSettingsPage RegionSettingsPageRoot => RequireSettingsPage(RegionSettingsPanel, nameof(RegionSettingsPanel)); - private WeatherSettingsPage WeatherSettingsPageRoot => RequireSettingsPage(WeatherSettingsPanel, nameof(WeatherSettingsPanel)); - private UpdateSettingsPage UpdateSettingsPageRoot => RequireSettingsPage(UpdateSettingsPanel, nameof(UpdateSettingsPanel)); - private AboutSettingsPage AboutSettingsPageRoot => RequireSettingsPage(AboutSettingsPanel, nameof(AboutSettingsPanel)); - private LauncherSettingsPage LauncherSettingsPageRoot => RequireSettingsPage(LauncherSettingsPanel, nameof(LauncherSettingsPanel)); - - private T WallpaperControl(string name) where T : Control => RequirePageControl(WallpaperSettingsPageRoot, name); - private T GridControl(string name) where T : Control => RequirePageControl(GridSettingsPageRoot, name); - private T ColorControl(string name) where T : Control => RequirePageControl(ColorSettingsPageRoot, name); - private T StatusBarControl(string name) where T : Control => RequirePageControl(StatusBarSettingsPageRoot, name); - private T RegionControl(string name) where T : Control => RequirePageControl(RegionSettingsPageRoot, name); - private T WeatherControl(string name) where T : Control => RequirePageControl(WeatherSettingsPageRoot, name); - private T UpdateControl(string name) where T : Control => RequirePageControl(UpdateSettingsPageRoot, name); - private T AboutControl(string name) where T : Control => RequirePageControl(AboutSettingsPageRoot, name); - private T LauncherControl(string name) where T : Control => RequirePageControl(LauncherSettingsPageRoot, name); - - internal TextBlock WallpaperPanelTitleTextBlock => WallpaperControl("WallpaperPanelTitleTextBlock"); - internal TextBlock WallpaperPathTextBlock => WallpaperControl("WallpaperPathTextBlock"); - internal TextBlock WallpaperStatusTextBlock => WallpaperControl("WallpaperStatusTextBlock"); - internal ComboBox WallpaperPlacementComboBox => WallpaperControl("WallpaperPlacementComboBox"); - internal Border WallpaperPreviewHost => WallpaperControl("WallpaperPreviewHost"); - internal Border WallpaperPreviewFrame => WallpaperControl("WallpaperPreviewFrame"); - internal Border WallpaperPreviewViewport => WallpaperControl("WallpaperPreviewViewport"); - internal Image WallpaperPreviewVideoImage => WallpaperControl("WallpaperPreviewVideoImage"); - internal Grid WallpaperPreviewGrid => WallpaperControl("WallpaperPreviewGrid"); - internal Border WallpaperPreviewTopStatusBarHost => WallpaperControl("WallpaperPreviewTopStatusBarHost"); - internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperControl("WallpaperPreviewTopStatusComponentsPanel"); - internal ClockWidget WallpaperPreviewClockWidget => WallpaperControl("WallpaperPreviewClockWidget"); - internal Border WallpaperPreviewBottomTaskbarContainer => WallpaperControl("WallpaperPreviewBottomTaskbarContainer"); - internal Border WallpaperPreviewTaskbarFixedActionsHost => WallpaperControl("WallpaperPreviewTaskbarFixedActionsHost"); - internal StackPanel WallpaperPreviewBackButtonVisual => WallpaperControl("WallpaperPreviewBackButtonVisual"); - internal TextBlock WallpaperPreviewBackButtonTextBlock => WallpaperControl("WallpaperPreviewBackButtonTextBlock"); - internal StackPanel WallpaperPreviewTaskbarDynamicActionsHost => WallpaperControl("WallpaperPreviewTaskbarDynamicActionsHost"); - internal Border WallpaperPreviewTaskbarSettingsActionHost => WallpaperControl("WallpaperPreviewTaskbarSettingsActionHost"); - internal StackPanel WallpaperPreviewComponentLibraryVisual => WallpaperControl("WallpaperPreviewComponentLibraryVisual"); - internal TextBlock WallpaperPreviewComponentLibraryTextBlock => WallpaperControl("WallpaperPreviewComponentLibraryTextBlock"); - internal FluentIcons.Avalonia.SymbolIcon WallpaperPreviewSettingsButtonIcon => WallpaperControl("WallpaperPreviewSettingsButtonIcon"); - internal Button PickWallpaperButton => WallpaperControl - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -