Files
LanMountainDesktop/LanMountainDesktop.PluginSdk/PluginServiceCollectionExtensions.cs
lincube aa7c118d13 Add external public IPC host/client and plugin SDK
Introduce a new LanMountainDesktop.Shared.IPC project implementing a public IPC host and client (LanMountainDesktopIpcClient, PublicIpcHostService), IPC constants and routed notify IDs, DTOs and DI helpers for registering public services. Update Plugin SDK to allow plugins to contribute public IPC services and registrations, add related descriptors/records and extension helpers. Migrate Launcher/App to use the new public IPC for startup/loading notifications and wiring (including TryConnect helper), switch LoadingStateReporter to use the external notification publisher, and add host-side public services (app info, shell control, plugin catalog). Include integration tests and spec/checklist/docs for the external IPC public API.
2026-04-22 14:55:30 +08:00

183 lines
6.8 KiB
C#

using Avalonia.Controls;
using dotnetCampus.Ipc.CompilerServices.Attributes;
using Microsoft.Extensions.DependencyInjection;
namespace LanMountainDesktop.PluginSdk;
public static class PluginServiceCollectionExtensions
{
public static IServiceCollection AddPluginSettingsSection(
this IServiceCollection services,
string id,
string titleLocalizationKey,
Action<PluginSettingsSectionBuilder> configure,
string? descriptionLocalizationKey = null,
string iconKey = "PuzzlePiece",
int sortOrder = 0)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
var builder = new PluginSettingsSectionBuilder(
id,
titleLocalizationKey,
descriptionLocalizationKey,
iconKey,
sortOrder);
configure(builder);
services.AddSingleton(builder.Build());
return services;
}
/// <summary>
/// Registers a plugin settings section with a custom AXAML view.
/// The host application will display <typeparamref name="TView"/> directly
/// in the settings window, allowing the plugin to use any Fluent Avalonia controls
/// and custom layouts — just like built-in settings pages.
/// </summary>
/// <typeparam name="TView">A <see cref="SettingsPageBase"/> subclass that defines the settings UI using AXAML.</typeparam>
public static IServiceCollection AddPluginSettingsSection<TView>(
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<TView>();
services.AddSingleton(builder.Build());
return services;
}
public static IServiceCollection AddPluginDesktopComponent<TControl>(
this IServiceCollection services,
PluginDesktopComponentOptions options)
where TControl : Control
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(options);
services.AddSingleton(new PluginDesktopComponentRegistration(
(provider, context) => ActivatorUtilities.CreateInstance<TControl>(provider, context),
options));
return services;
}
public static IServiceCollection AddPluginDesktopComponentEditor<TControl>(
this IServiceCollection services,
string componentId,
double preferredWidth = 720d,
double preferredHeight = 540d,
double minScale = 0.85d,
double maxScale = 1.45d)
where TControl : Control
{
ArgumentNullException.ThrowIfNull(services);
services.AddSingleton(new PluginDesktopComponentEditorRegistration(
componentId,
(provider, context) => ActivatorUtilities.CreateInstance<TControl>(provider, context),
preferredWidth,
preferredHeight,
minScale,
maxScale));
return services;
}
public static IServiceCollection AddPluginExport<TContract, TImplementation>(this IServiceCollection services)
where TContract : class
where TImplementation : class, TContract
{
ArgumentNullException.ThrowIfNull(services);
EnsureSingletonRegistration<TContract, TImplementation>(services);
if (!services.Any(descriptor =>
descriptor.ServiceType == typeof(PluginServiceExportRegistration) &&
descriptor.ImplementationInstance is PluginServiceExportRegistration existing &&
existing.ContractType == typeof(TContract) &&
existing.ImplementationType == typeof(TImplementation)))
{
services.AddSingleton(new PluginServiceExportRegistration(typeof(TContract), typeof(TImplementation)));
}
return services;
}
public static IServiceCollection AddPluginPublicIpc<TContract, TImplementation>(
this IServiceCollection services,
string? objectId = null,
params string[] notifyIds)
where TContract : class
where TImplementation : class, TContract
{
ArgumentNullException.ThrowIfNull(services);
EnsurePublicIpcContract(typeof(TContract));
EnsureSingletonRegistration<TContract, TImplementation>(services);
if (!services.Any(descriptor =>
descriptor.ServiceType == typeof(PluginPublicIpcServiceRegistration) &&
descriptor.ImplementationInstance is PluginPublicIpcServiceRegistration existing &&
existing.ContractType == typeof(TContract) &&
string.Equals(existing.ObjectId, objectId, StringComparison.Ordinal)))
{
services.AddSingleton(new PluginPublicIpcServiceRegistration(
typeof(TContract),
objectId,
notifyIds ?? []));
}
return services;
}
public static IServiceCollection AddPluginPublicIpcContributor<TContributor>(this IServiceCollection services)
where TContributor : class, IPluginPublicIpcContributor
{
ArgumentNullException.ThrowIfNull(services);
services.AddSingleton<IPluginPublicIpcContributor, TContributor>();
return services;
}
private static void EnsurePublicIpcContract(Type contractType)
{
if (!contractType.IsInterface)
{
throw new InvalidOperationException(
$"Public IPC contract '{contractType.FullName}' must be an interface.");
}
if (!Attribute.IsDefined(contractType, typeof(IpcPublicAttribute), inherit: false))
{
throw new InvalidOperationException(
$"Public IPC contract '{contractType.FullName}' must be marked with '{nameof(IpcPublicAttribute)}'.");
}
}
private static void EnsureSingletonRegistration<TContract, TImplementation>(IServiceCollection services)
where TContract : class
where TImplementation : class, TContract
{
var contractDescriptor = services.LastOrDefault(descriptor => descriptor.ServiceType == typeof(TContract));
if (contractDescriptor is null)
{
services.AddSingleton<TContract, TImplementation>();
return;
}
if (contractDescriptor.Lifetime != ServiceLifetime.Singleton)
{
throw new InvalidOperationException(
$"Exported contract '{typeof(TContract).FullName}' must be registered as Singleton.");
}
}
}