From 99a82d64e39574e14ed3b2c8364f07dcb715e403 Mon Sep 17 00:00:00 2001 From: lincube Date: Mon, 13 Apr 2026 01:23:11 +0800 Subject: [PATCH] =?UTF-8?q?change.=E6=8F=92=E4=BB=B6=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E6=94=AF=E6=8C=81View?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PluginServiceCollectionExtensions.cs | 29 ++++++++++++ .../PluginSettingsSectionBuilder.cs | 44 ++++++++++++++++++- .../PluginSettingsSectionRegistration.cs | 20 ++++++++- .../content/Plugin.cs | 32 ++++++++++++++ .../Services/Settings/SettingsPageRegistry.cs | 35 ++++++++++----- 5 files changed, 147 insertions(+), 13 deletions(-) diff --git a/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs b/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs index 48148d2..d1c872b 100644 --- a/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs +++ b/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs @@ -28,6 +28,35 @@ public static class PluginServiceCollectionExtensions return services; } + /// + /// Registers a plugin settings section with a custom AXAML view. + /// The host application will display directly + /// in the settings window, allowing the plugin to use any Fluent Avalonia controls + /// and custom layouts — just like built-in settings pages. + /// + /// A subclass that defines the settings UI using AXAML. + public static IServiceCollection AddPluginSettingsSection( + this IServiceCollection services, + string id, + string titleLocalizationKey, + string? descriptionLocalizationKey = null, + string iconKey = "PuzzlePiece", + int sortOrder = 0) + where TView : SettingsPageBase + { + ArgumentNullException.ThrowIfNull(services); + + var builder = new PluginSettingsSectionBuilder( + id, + titleLocalizationKey, + descriptionLocalizationKey, + iconKey, + sortOrder); + builder.SetCustomView(); + services.AddSingleton(builder.Build()); + return services; + } + public static IServiceCollection AddPluginDesktopComponent( this IServiceCollection services, PluginDesktopComponentOptions options) diff --git a/LanMountainDesktop.PluginSdk/PluginSettingsSectionBuilder.cs b/LanMountainDesktop.PluginSdk/PluginSettingsSectionBuilder.cs index 878a7c7..5450a5f 100644 --- a/LanMountainDesktop.PluginSdk/PluginSettingsSectionBuilder.cs +++ b/LanMountainDesktop.PluginSdk/PluginSettingsSectionBuilder.cs @@ -1,10 +1,13 @@ +using System; using System.Collections.Generic; +using Avalonia.Controls; namespace LanMountainDesktop.PluginSdk; public sealed class PluginSettingsSectionBuilder { private readonly List _options = []; + private Type? _customViewType; internal PluginSettingsSectionBuilder( string id, @@ -30,8 +33,46 @@ public sealed class PluginSettingsSectionBuilder public int SortOrder { get; } + public Type? CustomViewType => _customViewType; + public IReadOnlyList Options => _options; + /// + /// Sets a custom AXAML view for this settings section. + /// The view type must be a subclass of . + /// When a custom view is provided, the host application will use it directly + /// instead of generating a page from the declared options, allowing the plugin + /// to use any Fluent Avalonia controls and custom layouts. + /// + /// A subclass that defines the settings UI. + public PluginSettingsSectionBuilder SetCustomView() where TView : SettingsPageBase + { + _customViewType = typeof(TView); + return this; + } + + /// + /// Sets a custom AXAML view for this settings section. + /// The view type must be a subclass of . + /// When a custom view is provided, the host application will use it directly + /// instead of generating a page from the declared options. + /// + /// A subclass type that defines the settings UI. + public PluginSettingsSectionBuilder SetCustomView(Type viewType) + { + ArgumentNullException.ThrowIfNull(viewType); + + if (!typeof(SettingsPageBase).IsAssignableFrom(viewType)) + { + throw new ArgumentException( + $"Custom view type must be a subclass of {nameof(SettingsPageBase)}.", + nameof(viewType)); + } + + _customViewType = viewType; + return this; + } + public PluginSettingsSectionBuilder AddOption(SettingsOptionDefinition option) { ArgumentNullException.ThrowIfNull(option); @@ -142,6 +183,7 @@ public sealed class PluginSettingsSectionBuilder _options.ToArray(), DescriptionLocalizationKey, IconKey, - SortOrder); + SortOrder, + _customViewType); } } diff --git a/LanMountainDesktop.PluginSdk/PluginSettingsSectionRegistration.cs b/LanMountainDesktop.PluginSdk/PluginSettingsSectionRegistration.cs index 3bc1676..4516d9d 100644 --- a/LanMountainDesktop.PluginSdk/PluginSettingsSectionRegistration.cs +++ b/LanMountainDesktop.PluginSdk/PluginSettingsSectionRegistration.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace LanMountainDesktop.PluginSdk; @@ -10,7 +11,8 @@ public sealed class PluginSettingsSectionRegistration IReadOnlyList options, string? descriptionLocalizationKey = null, string iconKey = "PuzzlePiece", - int sortOrder = 0) + int sortOrder = 0, + Type? customViewType = null) { ArgumentException.ThrowIfNullOrWhiteSpace(id); ArgumentException.ThrowIfNullOrWhiteSpace(titleLocalizationKey); @@ -24,6 +26,15 @@ public sealed class PluginSettingsSectionRegistration IconKey = iconKey.Trim(); SortOrder = sortOrder; Options = options ?? []; + + if (customViewType is not null && !typeof(SettingsPageBase).IsAssignableFrom(customViewType)) + { + throw new ArgumentException( + $"Custom view type must be a subclass of {nameof(SettingsPageBase)}.", + nameof(customViewType)); + } + + CustomViewType = customViewType; } public string Id { get; } @@ -37,4 +48,11 @@ public sealed class PluginSettingsSectionRegistration public int SortOrder { get; } public IReadOnlyList Options { get; } + + /// + /// When set, the host application will instantiate this subclass + /// instead of generating a page from . + /// This allows plugins to provide fully custom AXAML views with any Fluent Avalonia controls. + /// + public Type? CustomViewType { get; } } diff --git a/LanMountainDesktop.PluginTemplate/content/Plugin.cs b/LanMountainDesktop.PluginTemplate/content/Plugin.cs index fe100eb..bd2037e 100644 --- a/LanMountainDesktop.PluginTemplate/content/Plugin.cs +++ b/LanMountainDesktop.PluginTemplate/content/Plugin.cs @@ -10,6 +10,38 @@ public sealed class Plugin : PluginBase public override void Initialize(HostBuilderContext context, IServiceCollection services) { _ = context; + + // ── Option 1: Declarative settings (simple key-value options) ────────── + // The host generates a settings page automatically from the declared options. + // Supported option types: Toggle, Text, Number, Select, Path, List. + // + // services.AddPluginSettingsSection( + // "my-plugin-settings", + // "My Plugin Settings", + // section => section + // .AddToggle("enable_feature", "Enable Feature", defaultValue: true) + // .AddNumber("refresh_interval", "Refresh Interval", defaultValue: 30, minimum: 5, maximum: 120), + // iconKey: "PuzzlePiece"); + + // ── Option 2: Custom AXAML view (full Fluent Avalonia controls) ──────── + // Provide a SettingsPageBase subclass to use any Fluent Avalonia control + // (SettingsExpander, ColorPicker, Slider, etc.) — just like built-in pages. + // + // services.AddPluginSettingsSection( + // "my-plugin-settings", + // "My Plugin Settings", + // iconKey: "PuzzlePiece"); + // + // Or mix both: declare options AND set a custom view on the builder: + // + // services.AddPluginSettingsSection( + // "my-plugin-settings", + // "My Plugin Settings", + // section => section + // .SetCustomView() + // .AddToggle("enable_feature", "Enable Feature"), + // iconKey: "PuzzlePiece"); + _ = services; } } diff --git a/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs b/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs index bc33847..043d18a 100644 --- a/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs +++ b/LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs @@ -256,6 +256,29 @@ internal sealed class SettingsPageRegistry : ISettingsPageRegistry, IDisposable ? null : localizer.GetString(section.DescriptionLocalizationKey, section.DescriptionLocalizationKey); + Func factory; + + if (section.CustomViewType is not null) + { + var customViewType = section.CustomViewType; + var pluginServices = loadedPlugin.Services; + factory = hostContext => CreatePage(pluginServices, customViewType, hostContext); + } + else + { + factory = hostContext => + { + var page = new GeneratedPluginSettingsPage( + new PluginGeneratedSettingsPageViewModel( + _settingsFacade.Settings, + loadedPlugin.Manifest.Id, + section, + localizer)); + page.InitializeHostContext(hostContext); + return page; + }; + } + _pages.Add(new SettingsPageDescriptor( pageId, title, @@ -270,17 +293,7 @@ internal sealed class SettingsPageRegistry : ISettingsPageRegistry, IDisposable hidePageTitle: false, useFullWidth: false, groupId: null, - hostContext => - { - var page = new GeneratedPluginSettingsPage( - new PluginGeneratedSettingsPageViewModel( - _settingsFacade.Settings, - loadedPlugin.Manifest.Id, - section, - localizer)); - page.InitializeHostContext(hostContext); - return page; - })); + factory)); } }