setting_re2

设置架构革新中
This commit is contained in:
lincube
2026-03-13 00:33:00 +08:00
parent 40a3a00cfe
commit c4df243610
92 changed files with 2048 additions and 10520 deletions

View File

@@ -21,7 +21,7 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
IPlugin plugin,
IPluginRuntimeContext runtimeContext,
IServiceProvider services,
IReadOnlyList<PluginSettingsPageRegistration> settingsPages,
IReadOnlyList<PluginSettingsSectionRegistration> settingsSections,
IReadOnlyList<PluginDesktopComponentRegistration> desktopComponents,
IReadOnlyList<PluginServiceExportDescriptor> exportedServices,
IReadOnlyList<IHostedService> hostedServices,
@@ -34,7 +34,7 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
Plugin = plugin;
RuntimeContext = runtimeContext;
Services = services;
SettingsPages = settingsPages;
SettingsSections = settingsSections;
DesktopComponents = desktopComponents;
ExportedServices = exportedServices;
HostedServices = hostedServices;
@@ -57,7 +57,7 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
public IServiceProvider Services { get; }
public IReadOnlyList<PluginSettingsPageRegistration> SettingsPages { get; }
public IReadOnlyList<PluginSettingsSectionRegistration> SettingsSections { get; }
public IReadOnlyList<PluginDesktopComponentRegistration> DesktopComponents { get; }

View File

@@ -1,166 +1,26 @@
using System;
using System.Collections.Generic;
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using FluentAvalonia.UI.Controls;
using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private readonly Dictionary<string, Control> _pluginSettingsPageHosts = new(StringComparer.OrdinalIgnoreCase);
private void InitializePluginSettingsNavigation()
{
if (_pluginSettingsPageHosts.Count > 0 || SettingsNavView?.MenuItems is null)
{
return;
}
var runtime = (Application.Current as App)?.PluginRuntimeService;
var contributions = runtime?.SettingsPages
.OrderBy(contribution => contribution.Registration.SortOrder)
.ThenBy(contribution => contribution.Plugin.Manifest.Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(contribution => contribution.Registration.Title, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (contributions is not { Length: > 0 })
{
return;
}
var pageCountsByPluginId = contributions
.GroupBy(contribution => contribution.Plugin.Manifest.Id, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
var insertIndex = SettingsNavView.MenuItems.IndexOf(SettingsNavPluginMarketItem) + 1;
foreach (var contribution in contributions)
{
var tag = BuildPluginSettingsTag(contribution);
var navigationTitle = BuildPluginSettingsNavigationTitle(contribution, pageCountsByPluginId);
var navItem = new NavigationViewItem
{
Content = navigationTitle,
Tag = tag,
IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = FluentIcons.Common.Symbol.PuzzlePiece,
IconVariant = FluentIcons.Common.IconVariant.Regular
}
};
ToolTip.SetTip(navItem, $"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}");
SettingsNavView.MenuItems.Insert(insertIndex++, navItem);
var pageHost = CreatePluginSettingsPageHost(contribution);
pageHost.IsVisible = false;
SettingsContentPagesHost.Children.Add(pageHost);
_pluginSettingsPageHosts[tag] = pageHost;
}
}
private static string BuildPluginSettingsTag(PluginSettingsPageContribution contribution)
{
return $"PluginPage:{contribution.Plugin.Manifest.Id}:{contribution.Registration.Id}";
}
private static string BuildPluginSettingsNavigationTitle(
PluginSettingsPageContribution contribution,
IReadOnlyDictionary<string, int> pageCountsByPluginId)
{
return pageCountsByPluginId.TryGetValue(contribution.Plugin.Manifest.Id, out var pageCount) && pageCount > 1
? $"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}"
: contribution.Plugin.Manifest.Name;
}
private Control CreatePluginSettingsPageHost(PluginSettingsPageContribution contribution)
{
Control content;
try
{
content = contribution.Registration.ContentFactory(contribution.Plugin.Services);
}
catch (Exception ex)
{
content = CreatePluginPageErrorContent(ex);
}
return new StackPanel
{
Spacing = 16,
Children =
{
new TextBlock
{
Text = contribution.Registration.Title,
FontSize = 24,
FontWeight = FontWeight.SemiBold,
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush")
},
new TextBlock
{
Text = contribution.Plugin.Manifest.Name,
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush")
},
content
}
};
}
private Control CreatePluginPageErrorContent(Exception exception)
{
return new Border
{
Background = new SolidColorBrush(Color.Parse("#332B0F16")),
BorderBrush = new SolidColorBrush(Color.Parse("#66F97316")),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(16),
Padding = new Thickness(16),
Child = new TextBlock
{
Text = exception.Message,
TextWrapping = TextWrapping.Wrap
}
};
// Legacy plugin settings pages are removed in API-only settings mode.
}
private void UpdatePluginSettingsPageVisibility(string? selectedTag)
{
foreach (var pair in _pluginSettingsPageHosts)
{
pair.Value.IsVisible = string.Equals(pair.Key, selectedTag, StringComparison.OrdinalIgnoreCase);
}
_ = selectedTag;
}
internal void RefreshPluginSettingsNavigation()
{
if (SettingsNavView?.MenuItems is null)
{
return;
}
foreach (var pair in _pluginSettingsPageHosts.ToArray())
{
var navItem = SettingsNavView.MenuItems
.OfType<NavigationViewItem>()
.FirstOrDefault(item => string.Equals(item.Tag?.ToString(), pair.Key, StringComparison.OrdinalIgnoreCase));
if (navItem is not null)
{
SettingsNavView.MenuItems.Remove(navItem);
}
SettingsContentPagesHost.Children.Remove(pair.Value);
}
_pluginSettingsPageHosts.Clear();
InitializePluginSettingsNavigation();
// Legacy plugin settings pages are removed in API-only settings mode.
}
private string? GetSelectedSettingsTabTag()
@@ -212,6 +72,3 @@ public partial class MainWindow
}
}
}

View File

@@ -3,9 +3,9 @@ using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services;
public sealed record PluginSettingsPageContribution(
public sealed record PluginSettingsSectionContribution(
LoadedPlugin Plugin,
PluginSettingsPageRegistration Registration);
PluginSettingsSectionRegistration Registration);
public sealed record PluginDesktopComponentContribution(
LoadedPlugin Plugin,

View File

@@ -170,10 +170,10 @@ public sealed class PluginLoader
AppLogger.Info("PluginLoader", $"Service provider built. PluginId='{manifest.Id}'.");
runtimeContext.SetServices(pluginServices);
var settingsPages = pluginServices
.GetServices<PluginSettingsPageRegistration>()
.OrderBy(page => page.SortOrder)
.ThenBy(page => page.Title, StringComparer.OrdinalIgnoreCase)
var settingsSections = pluginServices
.GetServices<PluginSettingsSectionRegistration>()
.OrderBy(section => section.SortOrder)
.ThenBy(section => section.TitleLocalizationKey, StringComparer.OrdinalIgnoreCase)
.ToArray();
var desktopComponents = pluginServices
.GetServices<PluginDesktopComponentRegistration>()
@@ -183,7 +183,7 @@ public sealed class PluginLoader
var exportedServices = ResolveExports(manifest, pluginServices);
AppLogger.Info(
"PluginLoader",
$"Plugin contributions resolved. PluginId='{manifest.Id}'; SettingsPages={settingsPages.Length}; Widgets={desktopComponents.Length}; Exports={exportedServices.Count}.");
$"Plugin contributions resolved. PluginId='{manifest.Id}'; SettingsSections={settingsSections.Length}; Widgets={desktopComponents.Length}; Exports={exportedServices.Count}.");
hostedServices = pluginServices.GetServices<IHostedService>().ToArray();
StartHostedServices(hostedServices);
AppLogger.Info("PluginLoader", $"Hosted services started. PluginId='{manifest.Id}'; HostedServices={hostedServices.Count}.");
@@ -196,7 +196,7 @@ public sealed class PluginLoader
plugin,
runtimeContext,
pluginServices,
settingsPages,
settingsSections,
desktopComponents,
exportedServices,
hostedServices,
@@ -314,6 +314,8 @@ public sealed class PluginLoader
RegisterHostService<IPluginPackageManager>(services, hostServices);
RegisterHostService<IHostApplicationLifecycle>(services, hostServices);
RegisterHostService<IPluginExportRegistry>(services, hostServices);
RegisterHostService<ISettingsService>(services, hostServices);
RegisterHostService<ISettingsCatalog>(services, hostServices);
return services;
}

View File

@@ -1,7 +1,7 @@
using System;
using System.IO;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketCacheService
{

View File

@@ -16,7 +16,7 @@ using Avalonia.Media.Imaging;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class PluginMarketEmbeddedView : UserControl, IDisposable
{

View File

@@ -5,7 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketIconService : IDisposable
{

View File

@@ -5,7 +5,7 @@ using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketIndexService : IDisposable
{

View File

@@ -8,7 +8,7 @@ using System.Security.Cryptography;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketInstallService : IDisposable
{

View File

@@ -6,7 +6,7 @@ using System.Linq;
using System.Text.Json;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal static class AirAppMarketDefaults
{

View File

@@ -4,7 +4,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketReadmeService : IDisposable
{

View File

@@ -5,7 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Services.PluginMarket;
internal sealed class AirAppMarketReleaseResolverService
{

View File

@@ -2,6 +2,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.PluginMarket;
namespace LanMountainDesktop.Views.SettingsPages;

View File

@@ -12,6 +12,7 @@ using Avalonia.Markup.Xaml;
using LanMountainDesktop.Models;
using LanMountainDesktop.Plugins;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -29,10 +30,12 @@ public sealed class PluginRuntimeService : IDisposable
private readonly PluginSharedContractManager _sharedContractManager;
private readonly IServiceProvider _hostServices;
private readonly IPluginPackageManager _packageManager;
private readonly SettingsFacadeService _settingsFacade;
private readonly SettingsCatalogService _settingsCatalogService;
private readonly List<LoadedPlugin> _loadedPlugins = [];
private readonly List<PluginLoadResult> _loadResults = [];
private readonly List<PluginCatalogEntry> _catalog = [];
private readonly List<PluginSettingsPageContribution> _settingsPages = [];
private readonly List<PluginSettingsSectionContribution> _settingsSections = [];
private readonly List<PluginDesktopComponentContribution> _desktopComponents = [];
private readonly object _packageMutationGate = new();
@@ -42,7 +45,14 @@ public sealed class PluginRuntimeService : IDisposable
_sharedContractManager = new PluginSharedContractManager(
Path.Combine(GetUserDataRootDirectory(), "PluginMarket"));
_packageManager = new PluginRuntimePackageManager(this);
_hostServices = new PluginHostServiceProvider(_packageManager, _applicationLifecycle, _exportRegistry);
_settingsFacade = new SettingsFacadeService(this);
_settingsCatalogService = (SettingsCatalogService)_settingsFacade.Catalog;
_hostServices = new PluginHostServiceProvider(
_packageManager,
_applicationLifecycle,
_exportRegistry,
_settingsFacade.Settings,
_settingsFacade.Catalog);
_loaderOptions = CreateOptions();
_loader = new PluginLoader(_loaderOptions);
}
@@ -55,12 +65,14 @@ public sealed class PluginRuntimeService : IDisposable
public IReadOnlyList<PluginCatalogEntry> Catalog => _catalog;
public IReadOnlyList<PluginSettingsPageContribution> SettingsPages => _settingsPages;
public IReadOnlyList<PluginSettingsSectionContribution> SettingsSections => _settingsSections;
public IReadOnlyList<PluginDesktopComponentContribution> DesktopComponents => _desktopComponents;
public IPluginExportRegistry ExportRegistry => _exportRegistry;
public ISettingsFacadeService SettingsFacade => _settingsFacade;
public void LoadInstalledPlugins()
{
Directory.CreateDirectory(PluginsDirectory);
@@ -172,11 +184,11 @@ public sealed class PluginRuntimeService : IDisposable
true,
true,
null,
loadResult.LoadedPlugin.SettingsPages.Count,
loadResult.LoadedPlugin.SettingsSections.Count,
loadResult.LoadedPlugin.DesktopComponents.Count));
AppLogger.Info(
"PluginRuntime",
$"Plugin loaded. PluginId='{loadResult.LoadedPlugin.Manifest.Id}'; SourcePath='{loadResult.SourcePath}'; ManifestVersion='{loadResult.LoadedPlugin.Manifest.Version ?? "<unknown>"}'; ApiVersion='{loadResult.LoadedPlugin.Manifest.ApiVersion ?? "<unknown>"}'; SourceKind='{candidate.SourceKind}'; SettingsPages={loadResult.LoadedPlugin.SettingsPages.Count}; Widgets={loadResult.LoadedPlugin.DesktopComponents.Count}.");
$"Plugin loaded. PluginId='{loadResult.LoadedPlugin.Manifest.Id}'; SourcePath='{loadResult.SourcePath}'; ManifestVersion='{loadResult.LoadedPlugin.Manifest.Version ?? "<unknown>"}'; ApiVersion='{loadResult.LoadedPlugin.Manifest.ApiVersion ?? "<unknown>"}'; SourceKind='{candidate.SourceKind}'; SettingsSections={loadResult.LoadedPlugin.SettingsSections.Count}; Widgets={loadResult.LoadedPlugin.DesktopComponents.Count}.");
Debug.WriteLine($"[PluginRuntime] Loaded '{loadResult.Manifest?.Id}' from '{loadResult.SourcePath}'.");
continue;
}
@@ -374,13 +386,16 @@ public sealed class PluginRuntimeService : IDisposable
{
UnloadInstalledPlugins();
_sharedContractManager.Dispose();
_settingsFacade.Dispose();
}
private void UnloadInstalledPlugins()
{
for (var i = _loadedPlugins.Count - 1; i >= 0; i--)
{
_exportRegistry.RemoveExports(_loadedPlugins[i].Manifest.Id);
var pluginId = _loadedPlugins[i].Manifest.Id;
_exportRegistry.RemoveExports(pluginId);
_settingsCatalogService.RemovePluginSections(pluginId);
_loadedPlugins[i].Dispose();
}
@@ -388,7 +403,7 @@ public sealed class PluginRuntimeService : IDisposable
_exportRegistry.Clear();
_loadResults.Clear();
_catalog.Clear();
_settingsPages.Clear();
_settingsSections.Clear();
_desktopComponents.Clear();
}
@@ -593,9 +608,16 @@ public sealed class PluginRuntimeService : IDisposable
{
_exportRegistry.ReplaceExports(loadedPlugin.Manifest.Id, loadedPlugin.ExportedServices);
foreach (var settingsPage in loadedPlugin.SettingsPages)
_settingsCatalogService.RegisterPluginSections(loadedPlugin.Manifest.Id, loadedPlugin.SettingsSections);
_settingsSections.RemoveAll(entry => string.Equals(
entry.Plugin.Manifest.Id,
loadedPlugin.Manifest.Id,
StringComparison.OrdinalIgnoreCase));
foreach (var settingsSection in loadedPlugin.SettingsSections)
{
_settingsPages.Add(new PluginSettingsPageContribution(loadedPlugin, settingsPage));
_settingsSections.Add(new PluginSettingsSectionContribution(loadedPlugin, settingsSection));
}
foreach (var desktopComponent in loadedPlugin.DesktopComponents)
@@ -769,9 +791,10 @@ public sealed class PluginRuntimeService : IDisposable
private void RemovePluginFromCatalog(string pluginId)
{
_catalog.RemoveAll(entry => string.Equals(entry.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_settingsPages.RemoveAll(entry => string.Equals(entry.Plugin.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_settingsSections.RemoveAll(entry => string.Equals(entry.Plugin.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_desktopComponents.RemoveAll(entry => string.Equals(entry.Plugin.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_loadResults.RemoveAll(entry => string.Equals(entry.Manifest?.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_settingsCatalogService.RemovePluginSections(pluginId);
}
private sealed record PluginCandidate(
@@ -784,15 +807,21 @@ public sealed class PluginRuntimeService : IDisposable
private readonly IPluginPackageManager _packageManager;
private readonly IHostApplicationLifecycle _applicationLifecycle;
private readonly IPluginExportRegistry _exportRegistry;
private readonly ISettingsService _settingsService;
private readonly ISettingsCatalog _settingsCatalog;
public PluginHostServiceProvider(
IPluginPackageManager packageManager,
IHostApplicationLifecycle applicationLifecycle,
IPluginExportRegistry exportRegistry)
IPluginExportRegistry exportRegistry,
ISettingsService settingsService,
ISettingsCatalog settingsCatalog)
{
_packageManager = packageManager;
_applicationLifecycle = applicationLifecycle;
_exportRegistry = exportRegistry;
_settingsService = settingsService;
_settingsCatalog = settingsCatalog;
}
public object? GetService(Type serviceType)
@@ -812,6 +841,16 @@ public sealed class PluginRuntimeService : IDisposable
return _exportRegistry;
}
if (serviceType == typeof(ISettingsService))
{
return _settingsService;
}
if (serviceType == typeof(ISettingsCatalog))
{
return _settingsCatalog;
}
return null;
}
}

View File

@@ -75,11 +75,11 @@ public partial class PluginSettingsPage : UserControl
var enabledCount = runtime.Catalog.Count(entry => entry.IsEnabled);
PluginSystemStatusTextBlock.Text = F(
"settings.plugins.summary_format",
"Detected {0} plugin(s); enabled {1}; loaded {2}; settings pages {3}; widgets {4}; failures {5}.",
"Detected {0} plugin(s); enabled {1}; loaded {2}; settings sections {3}; widgets {4}; failures {5}.",
runtime.Catalog.Count,
enabledCount,
runtime.Catalog.Count(entry => entry.IsLoaded),
runtime.SettingsPages.Count,
runtime.SettingsSections.Count,
runtime.DesktopComponents.Count,
failures.Length);
@@ -316,9 +316,6 @@ public partial class PluginSettingsPage : UserControl
case MainWindow mainWindow:
mainWindow.RefreshPluginSettingsNavigation();
break;
case SettingsWindow settingsWindow:
settingsWindow.RefreshPluginSettingsNavigation();
break;
}
}

View File

@@ -9,7 +9,7 @@ using System.Security.Cryptography;
using System.Threading;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Views.SettingsPages;
using LanMountainDesktop.Services.PluginMarket;
namespace LanMountainDesktop.Plugins;

View File

@@ -1,9 +0,0 @@
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private void ApplyPluginMarketSettingsLocalization()
{
PluginMarketSettingsPanel.RefreshFromRuntime();
}
}

View File

@@ -1,15 +0,0 @@
using Avalonia.Controls;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
internal TextBlock PluginSettingsPanelTitleTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginSettingsPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander PluginSystemSettingsExpander => PluginSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("PluginSystemSettingsExpander")!;
internal TextBlock PluginSystemDescriptionTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginSystemDescriptionTextBlock")!;
internal TextBlock PluginSystemStatusTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginSystemStatusTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander InstalledPluginsSettingsExpander => PluginSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("InstalledPluginsSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander ImportPluginPackageSettingsExpander => PluginSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("ImportPluginPackageSettingsExpander")!;
internal TextBlock PluginRestartHintTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginRestartHintTextBlock")!;
internal TextBlock PluginCatalogEmptyTextBlock => PluginSettingsPanel.FindControl<TextBlock>("PluginCatalogEmptyTextBlock")!;
}

View File

@@ -1,198 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using FluentIcons.Common;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private readonly Dictionary<string, Control> _pluginSettingsPageHosts = new(StringComparer.OrdinalIgnoreCase);
private void InitializePluginSettingsNavigation()
{
_pluginSettingsPageHosts.Clear();
_pluginSettingsNavItems.Clear();
}
private void RegisterPluginSettingsDefinitions()
{
var runtime = (Application.Current as App)?.PluginRuntimeService;
var contributions = runtime?.SettingsPages
.OrderBy(contribution => contribution.Registration.SortOrder)
.ThenBy(contribution => contribution.Plugin.Manifest.Name, StringComparer.OrdinalIgnoreCase)
.ThenBy(contribution => contribution.Registration.Title, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (contributions is not { Length: > 0 })
{
return;
}
var pageCountsByPluginId = contributions
.GroupBy(contribution => contribution.Plugin.Manifest.Id, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < contributions.Length; i++)
{
var contribution = contributions[i];
var tag = BuildPluginSettingsTag(contribution);
_pluginSettingsPageHosts[tag] = CreatePluginSettingsPageHost(contribution);
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
tag,
BuildPluginSettingsNavigationTitle(contribution, pageCountsByPluginId),
BuildPluginSettingsPageDescription(contribution),
FluentIcons.Common.Symbol.PuzzlePiece,
IndependentSettingsPageCategory.External,
200 + i,
$"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}"));
}
}
private static string BuildPluginSettingsTag(PluginSettingsPageContribution contribution)
{
return $"PluginPage:{contribution.Plugin.Manifest.Id}:{contribution.Registration.Id}";
}
private static string BuildPluginSettingsNavigationTitle(
PluginSettingsPageContribution contribution,
IReadOnlyDictionary<string, int> pageCountsByPluginId)
{
return pageCountsByPluginId.TryGetValue(contribution.Plugin.Manifest.Id, out var pageCount) && pageCount > 1
? $"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}"
: contribution.Plugin.Manifest.Name;
}
private string BuildPluginSettingsPageDescription(PluginSettingsPageContribution contribution)
{
return Lf(
"settings.page_desc.plugin_contributed_format",
"Settings page '{0}' is provided by plugin '{1}'.",
contribution.Registration.Title,
contribution.Plugin.Manifest.Name);
}
private Control CreatePluginSettingsPageHost(PluginSettingsPageContribution contribution)
{
Control content;
try
{
content = contribution.Registration.ContentFactory(contribution.Plugin.Services);
}
catch (Exception ex)
{
content = CreatePluginPageErrorContent(ex);
}
return new StackPanel
{
Spacing = 16,
MaxWidth = 920,
Children =
{
new TextBlock
{
Text = contribution.Registration.Title,
FontSize = 24,
FontWeight = FontWeight.SemiBold,
Foreground = GetThemeBrush("TextFillColorPrimaryBrush")
},
new TextBlock
{
Text = contribution.Plugin.Manifest.Name,
Foreground = GetThemeBrush("TextFillColorSecondaryBrush")
},
content
}
};
}
private static Control CreatePluginPageErrorContent(Exception exception)
{
return new Border
{
Background = new SolidColorBrush(Color.Parse("#332B0F16")),
BorderBrush = new SolidColorBrush(Color.Parse("#66F97316")),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(16),
Padding = new Thickness(16),
Child = new TextBlock
{
Text = exception.Message,
TextWrapping = TextWrapping.Wrap
}
};
}
internal void RefreshPluginSettingsNavigation()
{
var preferredTag = NormalizeSettingsPageTag(_selectedSettingsTabTag);
InitializeSettingsNavigation();
SelectSettingsTab(
_settingsPageDefinitions.ContainsKey(preferredTag) ? preferredTag : "Plugins",
persistSelection: false);
PluginSettingsPanel?.RefreshFromRuntime();
}
private string? GetSelectedSettingsTabTag()
{
return NormalizeSettingsPageTag(_selectedSettingsTabTag);
}
private int ResolveSelectedSettingsTabIndex()
{
if (SettingsNavView?.MenuItems is null)
{
return 0;
}
var items = SettingsNavView.MenuItems.OfType<NavigationViewItem>().ToList();
for (var i = 0; i < items.Count; i++)
{
if (string.Equals(items[i].Tag?.ToString(), NormalizeSettingsPageTag(_selectedSettingsTabTag), StringComparison.OrdinalIgnoreCase))
{
return i;
}
}
return 0;
}
private void RestoreSettingsTabSelection(AppSettingsSnapshot snapshot)
{
if (SettingsNavView?.MenuItems is null || SettingsNavView.MenuItems.Count == 0)
{
return;
}
var items = SettingsNavView.MenuItems.OfType<NavigationViewItem>().ToList();
if (items.Count == 0)
{
return;
}
if (!string.IsNullOrWhiteSpace(snapshot.SettingsTabTag))
{
var normalizedTag = NormalizeSettingsPageTag(snapshot.SettingsTabTag);
var taggedItem = items
.FirstOrDefault(item => string.Equals(item.Tag?.ToString(), normalizedTag, StringComparison.OrdinalIgnoreCase));
if (taggedItem is not null)
{
_selectedSettingsTabTag = normalizedTag;
SettingsNavView.SelectedItem = taggedItem;
return;
}
}
var safeIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, Math.Max(0, items.Count - 1));
_selectedSettingsTabTag = items[safeIndex].Tag?.ToString() ?? _selectedSettingsTabTag;
SettingsNavView.SelectedItem = items[safeIndex];
}
}

View File

@@ -1,20 +0,0 @@
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private void ApplyPluginSettingsLocalization()
{
PluginSettingsPanelTitleTextBlock.Text = L("settings.plugins.title", "Plugins");
PluginSystemSettingsExpander.Header = L("settings.plugins.runtime_header", "Plugin Runtime");
PluginSystemSettingsExpander.Description = L("settings.plugins.runtime_desc", "Review plugin runtime state and load results.");
PluginSystemDescriptionTextBlock.Text = L("settings.plugins.runtime_hint", "This page shows discovery status, load results, and runtime diagnostics for installed plugins.");
PluginSystemStatusTextBlock.Text = L("settings.plugins.runtime_status", "Plugin runtime status will appear here after plugin discovery completes.");
InstalledPluginsSettingsExpander.Header = L("settings.plugins.installed_header", "Installed Plugins");
InstalledPluginsSettingsExpander.Description = L("settings.plugins.installed_desc", "Review installed plugins and remove them here.");
ImportPluginPackageSettingsExpander.Header = L("settings.plugins.import_header", "Install From Package");
ImportPluginPackageSettingsExpander.Description = L("settings.plugins.import_desc", "Open a .laapp package and stage it into the local plugin directory.");
PluginRestartHintTextBlock.Text = L("settings.plugins.restart_hint", "Plugin installation and deletion changes take effect after restarting the app.");
PluginCatalogEmptyTextBlock.Text = L("settings.plugins.empty", "No plugins found.");
PluginSettingsPanel.RefreshFromRuntime();
}
}