This commit is contained in:
lincube
2026-03-20 22:37:37 +08:00
parent 20cd6041a7
commit 33baaa579d
92 changed files with 1149 additions and 2752 deletions

View File

@@ -0,0 +1,10 @@
namespace LanMountainDesktop.PluginSdk;
public interface IPluginAppearanceContext
{
PluginAppearanceSnapshot Snapshot { get; }
double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null);
double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null);
}

View File

@@ -12,6 +12,8 @@ public interface IPluginRuntimeContext
IReadOnlyDictionary<string, object?> Properties { get; }
IPluginAppearanceContext Appearance { get; }
T? GetService<T>();
bool TryGetProperty<T>(string key, out T? value);

View File

@@ -0,0 +1,49 @@
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginAppearanceContext : IPluginAppearanceContext
{
public PluginAppearanceContext(PluginAppearanceSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);
ArgumentNullException.ThrowIfNull(snapshot.CornerRadiusTokens);
Snapshot = snapshot with
{
GlobalCornerRadiusScale = Math.Max(0d, snapshot.GlobalCornerRadiusScale),
ThemeVariant = string.IsNullOrWhiteSpace(snapshot.ThemeVariant)
? "Unknown"
: snapshot.ThemeVariant.Trim()
};
}
public PluginAppearanceSnapshot Snapshot { get; }
public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null)
{
var scale = Snapshot.GlobalCornerRadiusScale;
var scaled = Math.Max(0d, baseRadius) * scale;
var scaledMin = minimum.HasValue ? minimum.Value * scale : scaled;
var scaledMax = maximum.HasValue ? maximum.Value * scale : scaled;
return minimum.HasValue || maximum.HasValue
? Math.Clamp(scaled, scaledMin, scaledMax)
: scaled;
}
public double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null)
{
var resolved = Math.Max(0d, Snapshot.CornerRadiusTokens.Get(preset));
if (!minimum.HasValue && !maximum.HasValue)
{
return resolved;
}
var clampedMin = minimum ?? resolved;
var clampedMax = maximum ?? resolved;
if (clampedMin > clampedMax)
{
(clampedMin, clampedMax) = (clampedMax, clampedMin);
}
return Math.Clamp(resolved, clampedMin, clampedMax);
}
}

View File

@@ -0,0 +1,6 @@
namespace LanMountainDesktop.PluginSdk;
public sealed record PluginAppearanceSnapshot(
double GlobalCornerRadiusScale,
PluginCornerRadiusTokens CornerRadiusTokens,
string ThemeVariant);

View File

@@ -0,0 +1,13 @@
namespace LanMountainDesktop.PluginSdk;
public enum PluginCornerRadiusPreset
{
Default = 0,
Micro = 1,
Xs = 2,
Sm = 3,
Md = 4,
Lg = 5,
Xl = 6,
Island = 7
}

View File

@@ -0,0 +1,49 @@
using Avalonia;
using LanMountainDesktop.Shared.Contracts;
namespace LanMountainDesktop.PluginSdk;
public sealed record PluginCornerRadiusTokens(
double Micro,
double Xs,
double Sm,
double Md,
double Lg,
double Xl,
double Island)
{
public double Get(PluginCornerRadiusPreset preset)
{
return preset switch
{
PluginCornerRadiusPreset.Default => Md,
PluginCornerRadiusPreset.Micro => Micro,
PluginCornerRadiusPreset.Xs => Xs,
PluginCornerRadiusPreset.Sm => Sm,
PluginCornerRadiusPreset.Md => Md,
PluginCornerRadiusPreset.Lg => Lg,
PluginCornerRadiusPreset.Xl => Xl,
PluginCornerRadiusPreset.Island => Island,
_ => Md
};
}
public CornerRadius ToCornerRadius(PluginCornerRadiusPreset preset)
{
return new CornerRadius(Get(preset));
}
public static PluginCornerRadiusTokens FromShared(AppearanceCornerRadiusTokens tokens)
{
ArgumentNullException.ThrowIfNull(tokens);
return new PluginCornerRadiusTokens(
tokens.Micro.TopLeft,
tokens.Xs.TopLeft,
tokens.Sm.TopLeft,
tokens.Md.TopLeft,
tokens.Lg.TopLeft,
tokens.Xl.TopLeft,
tokens.Island.TopLeft);
}
}

View File

@@ -1,5 +1,3 @@
using LanMountainDesktop.Shared.Contracts;
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginDesktopComponentContext
@@ -13,8 +11,7 @@ public sealed class PluginDesktopComponentContext
string componentId,
string? placementId,
double cellSize,
double globalCornerRadiusScale,
AppearanceCornerRadiusTokens cornerRadiusTokens,
IPluginAppearanceContext appearance,
IPluginSettingsService? pluginSettings = null)
{
ArgumentNullException.ThrowIfNull(manifest);
@@ -23,7 +20,7 @@ public sealed class PluginDesktopComponentContext
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(properties);
ArgumentNullException.ThrowIfNull(cornerRadiusTokens);
ArgumentNullException.ThrowIfNull(appearance);
Manifest = manifest;
PluginDirectory = pluginDirectory;
@@ -33,8 +30,7 @@ public sealed class PluginDesktopComponentContext
ComponentId = componentId.Trim();
PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim();
CellSize = Math.Max(1, cellSize);
GlobalCornerRadiusScale = Math.Max(0d, globalCornerRadiusScale);
CornerRadiusTokens = cornerRadiusTokens;
Appearance = appearance;
PluginSettings = pluginSettings;
}
@@ -54,20 +50,22 @@ public sealed class PluginDesktopComponentContext
public double CellSize { get; }
public double GlobalCornerRadiusScale { get; }
public IPluginAppearanceContext Appearance { get; }
public AppearanceCornerRadiusTokens CornerRadiusTokens { get; }
public double GlobalCornerRadiusScale => Appearance.Snapshot.GlobalCornerRadiusScale;
public PluginCornerRadiusTokens CornerRadiusTokens => Appearance.Snapshot.CornerRadiusTokens;
public IPluginSettingsService? PluginSettings { get; }
public double ResolveScaledCornerRadius(double baseRadius, double? minimum = null, double? maximum = null)
{
var scaled = Math.Max(0d, baseRadius) * GlobalCornerRadiusScale;
var scaledMin = minimum.HasValue ? minimum.Value * GlobalCornerRadiusScale : scaled;
var scaledMax = maximum.HasValue ? maximum.Value * GlobalCornerRadiusScale : scaled;
return minimum.HasValue || maximum.HasValue
? Math.Clamp(scaled, scaledMin, scaledMax)
: scaled;
return Appearance.ResolveScaledCornerRadius(baseRadius, minimum, maximum);
}
public double ResolveCornerRadius(PluginCornerRadiusPreset preset, double? minimum = null, double? maximum = null)
{
return Appearance.ResolveCornerRadius(preset, minimum, maximum);
}
public T? GetService<T>()

View File

@@ -0,0 +1,28 @@
namespace LanMountainDesktop.PluginSdk;
public sealed class PluginDesktopComponentOptions
{
public required string ComponentId { get; init; }
public required string DisplayName { get; init; }
public string IconKey { get; init; } = "PuzzlePiece";
public string Category { get; init; } = "Plugins";
public int MinWidthCells { get; init; } = 2;
public int MinHeightCells { get; init; } = 2;
public bool AllowDesktopPlacement { get; init; } = true;
public bool AllowStatusBarPlacement { get; init; }
public PluginDesktopComponentResizeMode ResizeMode { get; init; } = PluginDesktopComponentResizeMode.Proportional;
public string? DisplayNameLocalizationKey { get; init; }
public PluginCornerRadiusPreset CornerRadiusPreset { get; init; } = PluginCornerRadiusPreset.Default;
public Func<IPluginAppearanceContext, double, double>? CornerRadiusResolver { get; init; }
}

View File

@@ -5,67 +5,37 @@ namespace LanMountainDesktop.PluginSdk;
public sealed class PluginDesktopComponentRegistration
{
public PluginDesktopComponentRegistration(
string componentId,
string displayName,
Func<IServiceProvider, PluginDesktopComponentContext, Control> controlFactory,
string iconKey = "PuzzlePiece",
string category = "Plugins",
int minWidthCells = 2,
int minHeightCells = 2,
bool allowDesktopPlacement = true,
bool allowStatusBarPlacement = false,
PluginDesktopComponentResizeMode resizeMode = PluginDesktopComponentResizeMode.Proportional,
string? displayNameLocalizationKey = null,
Func<double, double>? cornerRadiusResolver = null)
PluginDesktopComponentOptions options)
{
ArgumentException.ThrowIfNullOrWhiteSpace(componentId);
ArgumentException.ThrowIfNullOrWhiteSpace(displayName);
ArgumentException.ThrowIfNullOrWhiteSpace(iconKey);
ArgumentException.ThrowIfNullOrWhiteSpace(category);
ArgumentNullException.ThrowIfNull(controlFactory);
ArgumentNullException.ThrowIfNull(options);
ArgumentException.ThrowIfNullOrWhiteSpace(options.ComponentId);
ArgumentException.ThrowIfNullOrWhiteSpace(options.DisplayName);
ArgumentException.ThrowIfNullOrWhiteSpace(options.IconKey);
ArgumentException.ThrowIfNullOrWhiteSpace(options.Category);
ComponentId = componentId.Trim();
DisplayName = displayName.Trim();
DisplayNameLocalizationKey = string.IsNullOrWhiteSpace(displayNameLocalizationKey)
ComponentId = options.ComponentId.Trim();
DisplayName = options.DisplayName.Trim();
DisplayNameLocalizationKey = string.IsNullOrWhiteSpace(options.DisplayNameLocalizationKey)
? null
: displayNameLocalizationKey.Trim();
: options.DisplayNameLocalizationKey.Trim();
ControlFactory = controlFactory;
IconKey = iconKey.Trim();
Category = category.Trim();
MinWidthCells = Math.Max(1, minWidthCells);
MinHeightCells = Math.Max(1, minHeightCells);
AllowDesktopPlacement = allowDesktopPlacement;
AllowStatusBarPlacement = allowStatusBarPlacement;
ResizeMode = resizeMode;
CornerRadiusResolver = cornerRadiusResolver;
IconKey = options.IconKey.Trim();
Category = options.Category.Trim();
MinWidthCells = Math.Max(1, options.MinWidthCells);
MinHeightCells = Math.Max(1, options.MinHeightCells);
AllowDesktopPlacement = options.AllowDesktopPlacement;
AllowStatusBarPlacement = options.AllowStatusBarPlacement;
ResizeMode = options.ResizeMode;
CornerRadiusPreset = options.CornerRadiusPreset;
CornerRadiusResolver = options.CornerRadiusResolver;
}
public PluginDesktopComponentRegistration(
string componentId,
string displayName,
Func<PluginDesktopComponentContext, Control> controlFactory,
string iconKey = "PuzzlePiece",
string category = "Plugins",
int minWidthCells = 2,
int minHeightCells = 2,
bool allowDesktopPlacement = true,
bool allowStatusBarPlacement = false,
PluginDesktopComponentResizeMode resizeMode = PluginDesktopComponentResizeMode.Proportional,
string? displayNameLocalizationKey = null,
Func<double, double>? cornerRadiusResolver = null)
: this(
componentId,
displayName,
(_, context) => controlFactory(context),
iconKey,
category,
minWidthCells,
minHeightCells,
allowDesktopPlacement,
allowStatusBarPlacement,
resizeMode,
displayNameLocalizationKey,
cornerRadiusResolver)
PluginDesktopComponentOptions options)
: this((_, context) => controlFactory(context), options)
{
}
@@ -91,5 +61,25 @@ public sealed class PluginDesktopComponentRegistration
public PluginDesktopComponentResizeMode ResizeMode { get; }
public Func<double, double>? CornerRadiusResolver { get; }
public PluginCornerRadiusPreset CornerRadiusPreset { get; }
public Func<IPluginAppearanceContext, double, double>? CornerRadiusResolver { get; }
public double ResolveCornerRadius(IPluginAppearanceContext appearance, double cellSize)
{
ArgumentNullException.ThrowIfNull(appearance);
var resolved = CornerRadiusResolver is not null
? CornerRadiusResolver(appearance, Math.Max(1d, cellSize))
: CornerRadiusPreset == PluginCornerRadiusPreset.Default
? appearance.ResolveScaledCornerRadius(
Math.Clamp(Math.Max(1d, cellSize) * 0.22, 8, 18),
8,
18)
: appearance.ResolveCornerRadius(CornerRadiusPreset);
return double.IsFinite(resolved)
? Math.Max(0d, resolved)
: appearance.ResolveCornerRadius(PluginCornerRadiusPreset.Default);
}
}

View File

@@ -87,8 +87,8 @@ public sealed record PluginManifest(
throw new InvalidOperationException(
$"Plugin '{normalized.Id}' targets API version '{normalized.ApiVersion}' (major {requestedVersion.Major}), " +
$"but the host provides '{PluginSdkInfo.ApiVersion}' (major {currentVersion.Major}). " +
$"This host only supports v{currentVersion.Major}.x plugins. " +
$"Migrate the plugin to API {PluginSdkInfo.ApiVersion} and rebuild the package.");
$"This host only supports v{currentVersion.Major}.x plugins and rejects v{requestedVersion.Major}.x packages by default. " +
$"Migrate the plugin manifest and code to API {PluginSdkInfo.ApiVersion}, then rebuild and republish the package.");
}
return normalized;

View File

@@ -2,7 +2,7 @@ namespace LanMountainDesktop.PluginSdk;
public static class PluginSdkInfo
{
public const string ApiVersion = "3.0.0";
public const string ApiVersion = "4.0.0";
public const string ManifestFileName = "plugin.json";
public const string PackageFileExtension = ".laapp";
public const string DataDirectoryName = "Data";

View File

@@ -30,34 +30,15 @@ public static class PluginServiceCollectionExtensions
public static IServiceCollection AddPluginDesktopComponent<TControl>(
this IServiceCollection services,
string componentId,
string displayName,
string iconKey = "PuzzlePiece",
string category = "Plugins",
int minWidthCells = 2,
int minHeightCells = 2,
bool allowDesktopPlacement = true,
bool allowStatusBarPlacement = false,
PluginDesktopComponentResizeMode resizeMode = PluginDesktopComponentResizeMode.Proportional,
string? displayNameLocalizationKey = null,
Func<double, double>? cornerRadiusResolver = null)
PluginDesktopComponentOptions options)
where TControl : Control
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(options);
services.AddSingleton(new PluginDesktopComponentRegistration(
componentId,
displayName,
(provider, context) => ActivatorUtilities.CreateInstance<TControl>(provider, context),
iconKey,
category,
minWidthCells,
minHeightCells,
allowDesktopPlacement,
allowStatusBarPlacement,
resizeMode,
displayNameLocalizationKey,
cornerRadiusResolver));
options));
return services;
}