Skip to content

Commit f6fc956

Browse files
committed
✨ Added IPluginResourceInclusion(#135)
resolves #135
1 parent 998b874 commit f6fc956

2 files changed

Lines changed: 137 additions & 28 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using Avalonia.Controls;
2+
using Avalonia.Styling;
3+
using PlugHub.Shared.Attributes;
4+
using PlugHub.Shared.Models.Plugins;
5+
6+
namespace PlugHub.Shared.Interfaces.Plugins
7+
{
8+
/// <summary>
9+
/// Describes a plugin component that provides Avalonia resource dictionaries (AXAML files)
10+
/// that must be loaded before the main UI initializes.
11+
/// Declares all dependency and ordering relationships for conflict-free, deterministic resource loading.
12+
/// </summary>
13+
/// <param name="PluginID">Unique identifier for the plugin providing this descriptor.</param>
14+
/// <param name="DescriptorID">Unique identifier for the descriptor.</param>
15+
/// <param name="Version">Version of the descriptor.</param>
16+
/// <param name="ResourceUri">URI of the AXAML resource to be loaded as a ResourceInclude.</param>
17+
/// <param name="BaseUri">Base URI for resolving relative resource paths (defaults to plugin's base URI).</param>
18+
/// <param name="Factory">Optional delegate that creates one or more <see cref="IResourceDictionary"/> or <see cref="IResourceProvider"/> instances at runtime.</param>
19+
/// <param name="LoadBefore">Descriptors that should be applied after this one to maintain order.</param>
20+
/// <param name="LoadAfter">Descriptors that should be applied before this one to maintain order.</param>
21+
/// <param name="DependsOn">Descriptors that this descriptor explicitly depends on.</param>
22+
/// <param name="ConflictsWith">Descriptors with which this descriptor cannot coexist.</param>
23+
public record PluginResourceIncludeDescriptor(
24+
Guid PluginID,
25+
Guid DescriptorID,
26+
string Version,
27+
string? ResourceUri = null,
28+
string? BaseUri = null,
29+
Func<IResourceDictionary>? Factory = null,
30+
IEnumerable<PluginDescriptorReference>? LoadBefore = null,
31+
IEnumerable<PluginDescriptorReference>? LoadAfter = null,
32+
IEnumerable<PluginDescriptorReference>? DependsOn = null,
33+
IEnumerable<PluginDescriptorReference>? ConflictsWith = null
34+
) : PluginDescriptor(PluginID, DescriptorID, Version, LoadBefore, LoadAfter, DependsOn, ConflictsWith);
35+
36+
/// <summary>
37+
/// Interface for plugins that supply Avalonia resource dictionaries.
38+
/// Provides descriptors for AXAML resource files (styles, themes, control templates, etc.)
39+
/// that need to be loaded during application bootstrap.
40+
/// </summary>
41+
[DescriptorProvider("GetResourceIncludeDescriptors", false)]
42+
public interface IPluginResourceInclusion : IPlugin
43+
{
44+
/// <summary>
45+
/// Returns a collection of descriptors defining Avalonia resource dictionaries
46+
/// (AXAML files containing styles, themes, or other resources) offered by this plugin.
47+
/// </summary>
48+
IEnumerable<PluginResourceIncludeDescriptor> GetResourceIncludeDescriptors();
49+
}
50+
}

PlugHub/App.axaml.cs

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -239,15 +239,7 @@ private static void ConfigureTheme(IConfigService configService, TokenSet tokenS
239239
ArgumentNullException.ThrowIfNull(configService);
240240
ArgumentNullException.ThrowIfNull(tokenSet);
241241

242-
IEnumerable<IPluginStyleInclusion> styleIncludeProviders = serviceProvider.GetServices<IPluginStyleInclusion>();
243242
ILogger<App> logger = serviceProvider.GetRequiredService<ILogger<App>>();
244-
IPluginResolver pluginResolver = serviceProvider.GetRequiredService<IPluginResolver>();
245-
246-
IReadOnlyList<PluginStyleIncludeDescriptor> orderedDescriptors =
247-
pluginResolver.ResolveAndOrder<IPluginStyleInclusion, PluginStyleIncludeDescriptor>(styleIncludeProviders);
248-
249-
HashSet<string> loadedResourceDictionaries = [];
250-
HashSet<Type> loadedFactoryTypes = [];
251243

252244
IConfigAccessorFor<AppConfig> configAccessor = configService.GetAccessor<AppConfig>(owner: tokenSet.Owner);
253245
IConfigAccessorFor<AppEnv> envAccessor = configService.GetAccessor<AppEnv>(owner: tokenSet.Owner);
@@ -264,9 +256,6 @@ private static void ConfigureTheme(IConfigService configService, TokenSet tokenS
264256
? appEnv.SystemTheme!
265257
: appConfig.SystemTheme;
266258

267-
if (!useDefaultTheme)
268-
return;
269-
270259
ThemeVariant requested = systemThemeStr?.Trim().ToLowerInvariant() switch
271260
{
272261
"light" => ThemeVariant.Light,
@@ -277,51 +266,121 @@ private static void ConfigureTheme(IConfigService configService, TokenSet tokenS
277266
if (Application.Current != null)
278267
Application.Current.RequestedThemeVariant = requested;
279268

280-
styles.Add(new StyleInclude(new Uri("avares://PlugHub/"))
269+
if (useDefaultTheme)
281270
{
282-
Source = new Uri("avares://PlugHub/Styles/Icons.axaml")
283-
});
271+
styles.Add(new FluentAvaloniaTheme
272+
{
273+
PreferSystemTheme = preferSystemTheme,
274+
PreferUserAccentColor = preferUserAccentColor
275+
});
276+
}
284277

285-
styles.Add(new FluentAvaloniaTheme
278+
resources.MergedDictionaries.Add(new ResourceInclude(new Uri("avares://PlugHub/"))
286279
{
287-
PreferSystemTheme = preferSystemTheme,
288-
PreferUserAccentColor = preferUserAccentColor
280+
Source = new Uri("avares://PlugHub/Styles/Generic.axaml")
289281
});
290282

291-
resources.MergedDictionaries.Add(new ResourceInclude(new Uri("avares://PlugHub/"))
283+
AddPluginResources(resources, logger);
284+
285+
styles.Add(new StyleInclude(new Uri("avares://PlugHub/"))
292286
{
293-
Source = new Uri("avares://PlugHub/Styles/Generic.axaml")
287+
Source = new Uri("avares://PlugHub/Styles/Icons.axaml")
294288
});
295289

290+
AddPluginStyles(styles, logger);
291+
}
292+
293+
private static void AddPluginResources(IResourceDictionary resources, ILogger<App> logger)
294+
{
295+
IEnumerable<IPluginResourceInclusion>? providers = serviceProvider?.GetServices<IPluginResourceInclusion>();
296+
IPluginResolver? resolver = serviceProvider?.GetRequiredService<IPluginResolver>();
297+
298+
IReadOnlyList<PluginResourceIncludeDescriptor>? descriptors = resolver?.ResolveAndOrder<IPluginResourceInclusion, PluginResourceIncludeDescriptor>(providers);
299+
300+
HashSet<string> loadedUris = [];
301+
HashSet<Type> loadedFactories = [];
296302

297-
foreach (PluginStyleIncludeDescriptor descriptor in orderedDescriptors)
303+
foreach (PluginResourceIncludeDescriptor descriptor in descriptors ?? [])
298304
{
299-
if (Application.Current?.Styles is null)
300-
continue;
305+
try
306+
{
307+
if (descriptor.Factory is not null)
308+
{
309+
IResourceDictionary? dict = descriptor.Factory();
301310

311+
if (dict is not null && loadedFactories.Add(dict.GetType()))
312+
{
313+
resources.MergedDictionaries.Add(dict);
314+
}
315+
else
316+
{
317+
logger.LogDebug("[App] Skipped duplicate resource factory of type {Type}", dict?.GetType().FullName);
318+
}
319+
}
320+
else if (!string.IsNullOrEmpty(descriptor.ResourceUri) && loadedUris.Add(descriptor.ResourceUri))
321+
{
322+
Uri baseUri = string.IsNullOrEmpty(descriptor.BaseUri)
323+
? new Uri("avares://PlugHub/")
324+
: new Uri(descriptor.BaseUri);
325+
326+
ResourceInclude include = new(baseUri)
327+
{
328+
Source = new Uri(descriptor.ResourceUri)
329+
};
330+
331+
resources.MergedDictionaries.Add(include);
332+
}
333+
}
334+
catch (Exception ex)
335+
{
336+
logger.LogError(ex, "[App] Failed to load resource from {Source}", descriptor.ResourceUri ?? descriptor.Factory?.Method?.Name ?? "unknown");
337+
}
338+
}
339+
340+
logger.LogInformation("[App] Added {UriCount} URI-based resource dictionaries and {FactoryCount} factory dictionaries", loadedUris.Count, loadedFactories.Count);
341+
}
342+
private static void AddPluginStyles(Styles styles, ILogger<App> logger)
343+
{
344+
if (Application.Current?.Styles is null)
345+
return;
346+
347+
IEnumerable<IPluginStyleInclusion>? providers = serviceProvider?.GetServices<IPluginStyleInclusion>();
348+
IPluginResolver? resolver = serviceProvider?.GetRequiredService<IPluginResolver>();
349+
350+
IReadOnlyList<PluginStyleIncludeDescriptor>? descriptors = resolver?.ResolveAndOrder<IPluginStyleInclusion, PluginStyleIncludeDescriptor>(providers);
351+
352+
HashSet<Type> loadedFactories = [];
353+
HashSet<string> loadedIncludes = [];
354+
355+
foreach (PluginStyleIncludeDescriptor descriptor in descriptors ?? [])
356+
{
302357
try
303358
{
304359
if (descriptor.Factory is not null)
305360
{
306361
IStyle style = descriptor.Factory();
307362

308-
if (loadedFactoryTypes.Add(style.GetType()))
309-
Application.Current.Styles.Add(style);
363+
if (loadedFactories.Add(style.GetType()))
364+
{
365+
styles.Add(style);
366+
}
310367
else
368+
{
311369
logger.LogDebug("[App] Skipped duplicate factory style of type {StyleType}", style.GetType().FullName);
370+
}
312371
}
313-
else if (!string.IsNullOrEmpty(descriptor.ResourceUri) && loadedResourceDictionaries.Add(descriptor.ResourceUri))
372+
else if (!string.IsNullOrEmpty(descriptor.ResourceUri) && loadedIncludes.Add(descriptor.ResourceUri))
314373
{
315374
Uri baseUri = string.IsNullOrEmpty(descriptor.BaseUri)
316375
? new Uri("avares://PlugHub/")
317376
: new Uri(descriptor.BaseUri);
318377

319-
StyleInclude styleInclude = new(baseUri)
378+
StyleInclude include = new(baseUri)
320379
{
321380
Source = new Uri(descriptor.ResourceUri)
322381
};
323382

324-
Application.Current.Styles.Add(styleInclude);
383+
styles.Add(include);
325384
}
326385
}
327386
catch (Exception ex)
@@ -330,7 +389,7 @@ private static void ConfigureTheme(IConfigService configService, TokenSet tokenS
330389
}
331390
}
332391

333-
logger.LogInformation("[App] PluginsStyleIncludes completed: Added {ResourceCount} unique resource dictionaries and {FactoryCount} unique factory styles.", loadedResourceDictionaries.Count, loadedFactoryTypes.Count);
392+
logger.LogInformation("[App] Added {FactoryCount} factory styles and {IncludeCount} style includes", loadedFactories.Count, loadedIncludes.Count);
334393
}
335394

336395
#endregion

0 commit comments

Comments
 (0)