diff --git a/PlugHub.Shared/Interfaces/Plugins/IPluginResourceInclusion.cs b/PlugHub.Shared/Interfaces/Plugins/IPluginResourceInclusion.cs
new file mode 100644
index 0000000..729c466
--- /dev/null
+++ b/PlugHub.Shared/Interfaces/Plugins/IPluginResourceInclusion.cs
@@ -0,0 +1,50 @@
+using Avalonia.Controls;
+using Avalonia.Styling;
+using PlugHub.Shared.Attributes;
+using PlugHub.Shared.Models.Plugins;
+
+namespace PlugHub.Shared.Interfaces.Plugins
+{
+ ///
+ /// Describes a plugin component that provides Avalonia resource dictionaries (AXAML files)
+ /// that must be loaded before the main UI initializes.
+ /// Declares all dependency and ordering relationships for conflict-free, deterministic resource loading.
+ ///
+ /// Unique identifier for the plugin providing this descriptor.
+ /// Unique identifier for the descriptor.
+ /// Version of the descriptor.
+ /// URI of the AXAML resource to be loaded as a ResourceInclude.
+ /// Base URI for resolving relative resource paths (defaults to plugin's base URI).
+ /// Optional delegate that creates one or more or instances at runtime.
+ /// Descriptors that should be applied after this one to maintain order.
+ /// Descriptors that should be applied before this one to maintain order.
+ /// Descriptors that this descriptor explicitly depends on.
+ /// Descriptors with which this descriptor cannot coexist.
+ public record PluginResourceIncludeDescriptor(
+ Guid PluginID,
+ Guid DescriptorID,
+ string Version,
+ string? ResourceUri = null,
+ string? BaseUri = null,
+ Func? Factory = null,
+ IEnumerable? LoadBefore = null,
+ IEnumerable? LoadAfter = null,
+ IEnumerable? DependsOn = null,
+ IEnumerable? ConflictsWith = null
+ ) : PluginDescriptor(PluginID, DescriptorID, Version, LoadBefore, LoadAfter, DependsOn, ConflictsWith);
+
+ ///
+ /// Interface for plugins that supply Avalonia resource dictionaries.
+ /// Provides descriptors for AXAML resource files (styles, themes, control templates, etc.)
+ /// that need to be loaded during application bootstrap.
+ ///
+ [DescriptorProvider("GetResourceIncludeDescriptors", false)]
+ public interface IPluginResourceInclusion : IPlugin
+ {
+ ///
+ /// Returns a collection of descriptors defining Avalonia resource dictionaries
+ /// (AXAML files containing styles, themes, or other resources) offered by this plugin.
+ ///
+ IEnumerable GetResourceIncludeDescriptors();
+ }
+}
\ No newline at end of file
diff --git a/PlugHub/App.axaml.cs b/PlugHub/App.axaml.cs
index c988ad1..bfb99c7 100644
--- a/PlugHub/App.axaml.cs
+++ b/PlugHub/App.axaml.cs
@@ -239,15 +239,7 @@ private static void ConfigureTheme(IConfigService configService, TokenSet tokenS
ArgumentNullException.ThrowIfNull(configService);
ArgumentNullException.ThrowIfNull(tokenSet);
- IEnumerable styleIncludeProviders = serviceProvider.GetServices();
ILogger logger = serviceProvider.GetRequiredService>();
- IPluginResolver pluginResolver = serviceProvider.GetRequiredService();
-
- IReadOnlyList orderedDescriptors =
- pluginResolver.ResolveAndOrder(styleIncludeProviders);
-
- HashSet loadedResourceDictionaries = [];
- HashSet loadedFactoryTypes = [];
IConfigAccessorFor configAccessor = configService.GetAccessor(owner: tokenSet.Owner);
IConfigAccessorFor envAccessor = configService.GetAccessor(owner: tokenSet.Owner);
@@ -264,9 +256,6 @@ private static void ConfigureTheme(IConfigService configService, TokenSet tokenS
? appEnv.SystemTheme!
: appConfig.SystemTheme;
- if (!useDefaultTheme)
- return;
-
ThemeVariant requested = systemThemeStr?.Trim().ToLowerInvariant() switch
{
"light" => ThemeVariant.Light,
@@ -277,51 +266,121 @@ private static void ConfigureTheme(IConfigService configService, TokenSet tokenS
if (Application.Current != null)
Application.Current.RequestedThemeVariant = requested;
- styles.Add(new StyleInclude(new Uri("avares://PlugHub/"))
+ if (useDefaultTheme)
{
- Source = new Uri("avares://PlugHub/Styles/Icons.axaml")
- });
+ styles.Add(new FluentAvaloniaTheme
+ {
+ PreferSystemTheme = preferSystemTheme,
+ PreferUserAccentColor = preferUserAccentColor
+ });
+ }
- styles.Add(new FluentAvaloniaTheme
+ resources.MergedDictionaries.Add(new ResourceInclude(new Uri("avares://PlugHub/"))
{
- PreferSystemTheme = preferSystemTheme,
- PreferUserAccentColor = preferUserAccentColor
+ Source = new Uri("avares://PlugHub/Styles/Generic.axaml")
});
- resources.MergedDictionaries.Add(new ResourceInclude(new Uri("avares://PlugHub/"))
+ AddPluginResources(resources, logger);
+
+ styles.Add(new StyleInclude(new Uri("avares://PlugHub/"))
{
- Source = new Uri("avares://PlugHub/Styles/Generic.axaml")
+ Source = new Uri("avares://PlugHub/Styles/Icons.axaml")
});
+ AddPluginStyles(styles, logger);
+ }
+
+ private static void AddPluginResources(IResourceDictionary resources, ILogger logger)
+ {
+ IEnumerable? providers = serviceProvider?.GetServices();
+ IPluginResolver? resolver = serviceProvider?.GetRequiredService();
+
+ IReadOnlyList? descriptors = resolver?.ResolveAndOrder(providers);
+
+ HashSet loadedUris = [];
+ HashSet loadedFactories = [];
- foreach (PluginStyleIncludeDescriptor descriptor in orderedDescriptors)
+ foreach (PluginResourceIncludeDescriptor descriptor in descriptors ?? [])
{
- if (Application.Current?.Styles is null)
- continue;
+ try
+ {
+ if (descriptor.Factory is not null)
+ {
+ IResourceDictionary? dict = descriptor.Factory();
+ if (dict is not null && loadedFactories.Add(dict.GetType()))
+ {
+ resources.MergedDictionaries.Add(dict);
+ }
+ else
+ {
+ logger.LogDebug("[App] Skipped duplicate resource factory of type {Type}", dict?.GetType().FullName);
+ }
+ }
+ else if (!string.IsNullOrEmpty(descriptor.ResourceUri) && loadedUris.Add(descriptor.ResourceUri))
+ {
+ Uri baseUri = string.IsNullOrEmpty(descriptor.BaseUri)
+ ? new Uri("avares://PlugHub/")
+ : new Uri(descriptor.BaseUri);
+
+ ResourceInclude include = new(baseUri)
+ {
+ Source = new Uri(descriptor.ResourceUri)
+ };
+
+ resources.MergedDictionaries.Add(include);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "[App] Failed to load resource from {Source}", descriptor.ResourceUri ?? descriptor.Factory?.Method?.Name ?? "unknown");
+ }
+ }
+
+ logger.LogInformation("[App] Added {UriCount} URI-based resource dictionaries and {FactoryCount} factory dictionaries", loadedUris.Count, loadedFactories.Count);
+ }
+ private static void AddPluginStyles(Styles styles, ILogger logger)
+ {
+ if (Application.Current?.Styles is null)
+ return;
+
+ IEnumerable? providers = serviceProvider?.GetServices();
+ IPluginResolver? resolver = serviceProvider?.GetRequiredService();
+
+ IReadOnlyList? descriptors = resolver?.ResolveAndOrder(providers);
+
+ HashSet loadedFactories = [];
+ HashSet loadedIncludes = [];
+
+ foreach (PluginStyleIncludeDescriptor descriptor in descriptors ?? [])
+ {
try
{
if (descriptor.Factory is not null)
{
IStyle style = descriptor.Factory();
- if (loadedFactoryTypes.Add(style.GetType()))
- Application.Current.Styles.Add(style);
+ if (loadedFactories.Add(style.GetType()))
+ {
+ styles.Add(style);
+ }
else
+ {
logger.LogDebug("[App] Skipped duplicate factory style of type {StyleType}", style.GetType().FullName);
+ }
}
- else if (!string.IsNullOrEmpty(descriptor.ResourceUri) && loadedResourceDictionaries.Add(descriptor.ResourceUri))
+ else if (!string.IsNullOrEmpty(descriptor.ResourceUri) && loadedIncludes.Add(descriptor.ResourceUri))
{
Uri baseUri = string.IsNullOrEmpty(descriptor.BaseUri)
? new Uri("avares://PlugHub/")
: new Uri(descriptor.BaseUri);
- StyleInclude styleInclude = new(baseUri)
+ StyleInclude include = new(baseUri)
{
Source = new Uri(descriptor.ResourceUri)
};
- Application.Current.Styles.Add(styleInclude);
+ styles.Add(include);
}
}
catch (Exception ex)
@@ -330,7 +389,7 @@ private static void ConfigureTheme(IConfigService configService, TokenSet tokenS
}
}
- logger.LogInformation("[App] PluginsStyleIncludes completed: Added {ResourceCount} unique resource dictionaries and {FactoryCount} unique factory styles.", loadedResourceDictionaries.Count, loadedFactoryTypes.Count);
+ logger.LogInformation("[App] Added {FactoryCount} factory styles and {IncludeCount} style includes", loadedFactories.Count, loadedIncludes.Count);
}
#endregion