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));
}
}