setting_re1

This commit is contained in:
lincube
2026-03-12 21:01:23 +08:00
parent 4679ee006f
commit 40a3a00cfe
42 changed files with 2367 additions and 1017 deletions

View File

@@ -145,22 +145,29 @@ public sealed class PluginLoader
{
Directory.CreateDirectory(dataDirectory);
ValidatePluginRuntimeAssets(manifest, assemblyPath, pluginDirectory);
AppLogger.Info(
"PluginLoader",
$"LoadCore starting. PluginId='{manifest.Id}'; AssemblyPath='{assemblyPath}'; PluginDirectory='{pluginDirectory}'; DataDirectory='{dataDirectory}'.");
loadContext = new PluginLoadContext(assemblyPath, _options.SharedAssemblyNames);
var assembly = loadContext.LoadFromAssemblyPath(assemblyPath);
AppLogger.Info("PluginLoader", $"Assembly loaded. PluginId='{manifest.Id}'; Assembly='{assembly.FullName}'.");
var pluginType = ResolvePluginType(assembly);
plugin = CreatePluginInstance(pluginType);
AppLogger.Info("PluginLoader", $"Plugin instance created. PluginId='{manifest.Id}'; PluginType='{pluginType.FullName}'.");
runtimeContext = CreateRuntimeContext(manifest, pluginDirectory, dataDirectory, properties);
var serviceCollection = CreateServiceCollection(runtimeContext, services);
var hostBuilderContext = CreateHostBuilderContext(runtimeContext);
plugin.Initialize(hostBuilderContext, serviceCollection);
AppLogger.Info("PluginLoader", $"Plugin Initialize completed. PluginId='{manifest.Id}'.");
pluginServices = serviceCollection.BuildServiceProvider(new ServiceProviderOptions
{
ValidateScopes = false,
ValidateOnBuild = true
});
AppLogger.Info("PluginLoader", $"Service provider built. PluginId='{manifest.Id}'.");
runtimeContext.SetServices(pluginServices);
var settingsPages = pluginServices
@@ -174,8 +181,12 @@ public sealed class PluginLoader
.ThenBy(component => component.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToArray();
var exportedServices = ResolveExports(manifest, pluginServices);
AppLogger.Info(
"PluginLoader",
$"Plugin contributions resolved. PluginId='{manifest.Id}'; SettingsPages={settingsPages.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}.");
var loadedPlugin = new LoadedPlugin(
manifest,
@@ -375,6 +386,7 @@ public sealed class PluginLoader
{
foreach (var hostedService in hostedServices)
{
AppLogger.Info("PluginLoader", $"Starting hosted service '{hostedService.GetType().FullName}'.");
hostedService.StartAsync(CancellationToken.None).GetAwaiter().GetResult();
}
}

View File

@@ -32,7 +32,7 @@ internal sealed class AirAppMarketIndexService : IDisposable
{
try
{
var json = await File.ReadAllTextAsync(localIndexPath, cancellationToken);
var json = await File.ReadAllTextAsync(localIndexPath, cancellationToken).ConfigureAwait(false);
var document = AirAppMarketIndexDocument.Load(json, localIndexPath);
_cacheService.SaveIndexJson(json);
return new AirAppMarketLoadResult(
@@ -61,8 +61,8 @@ internal sealed class AirAppMarketIndexService : IDisposable
{
using var response = await _httpClient.GetAsync(
AirAppMarketDefaults.DefaultIndexUrl,
cancellationToken);
var json = await response.Content.ReadAsStringAsync(cancellationToken);
cancellationToken).ConfigureAwait(false);
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var document = AirAppMarketIndexDocument.Load(json, AirAppMarketDefaults.DefaultIndexUrl);

View File

@@ -31,14 +31,8 @@ internal static class AirAppMarketDefaults
public static string? TryGetWorkspaceIndexPath()
{
var repositoryRoot = TryGetWorkspaceRepositoryRoot("LanAirApp");
if (repositoryRoot is null)
{
return null;
}
var candidatePath = Path.Combine(repositoryRoot, "airappmarket", "index.json");
return File.Exists(candidatePath) ? candidatePath : null;
var relativePath = Path.Combine("airappmarket", "index.json");
return TryResolveWorkspacePath("LanAirApp", relativePath);
}
public static bool TryResolveWorkspaceFile(string url, out string localPath)
@@ -57,14 +51,8 @@ internal static class AirAppMarketDefaults
return false;
}
var repositoryRoot = TryGetWorkspaceRepositoryRoot(repositoryName);
if (repositoryRoot is null)
{
return false;
}
var candidatePath = Path.GetFullPath(Path.Combine(repositoryRoot, relativePath));
if (!File.Exists(candidatePath))
var candidatePath = TryResolveWorkspacePath(repositoryName, relativePath);
if (candidatePath is null)
{
return false;
}
@@ -99,7 +87,7 @@ internal static class AirAppMarketDefaults
return !string.IsNullOrWhiteSpace(owner) && !string.IsNullOrWhiteSpace(repositoryName);
}
private static string? TryGetWorkspaceRepositoryRoot(string repositoryName)
private static string? TryResolveWorkspacePath(string repositoryName, string relativePath)
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
@@ -107,7 +95,11 @@ internal static class AirAppMarketDefaults
var candidate = Path.Combine(current.FullName, repositoryName);
if (Directory.Exists(candidate))
{
return candidate;
var candidatePath = Path.GetFullPath(Path.Combine(candidate, relativePath));
if (File.Exists(candidatePath))
{
return candidatePath;
}
}
current = current.Parent;

View File

@@ -1,25 +1,43 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
mc:Ignorable="d"
d:DesignWidth="960"
d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.PluginMarketSettingsPage">
<StackPanel x:Name="PluginMarketPanel"
MaxWidth="920"
Spacing="16">
<TextBlock x:Name="PluginMarketPanelTitleTextBlock"
IsVisible="False"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Plugin Market" />
<TextBlock x:Name="PluginMarketPanelSubtitleTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Browse plugins from the official LanAirApp source and stage installs." />
Text="Browse plugins from the official LanAirApp source, review package details, and stage installations safely." />
<ContentControl x:Name="PluginMarketContentHost" />
<Border Classes="settings-expander-shell"
Padding="16,14">
<StackPanel Spacing="10">
<TextBlock x:Name="PluginMarketSectionTitleTextBlock"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Official Source" />
<TextBlock x:Name="PluginMarketSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="The content below is loaded from the official market source. If network loading fails, the module will keep the page alive and show a recoverable error state instead of crashing." />
<ContentControl x:Name="PluginMarketContentHost" />
</StackPanel>
</Border>
</StackPanel>
</UserControl>

View File

@@ -66,6 +66,7 @@ public sealed class PluginRuntimeService : IDisposable
Directory.CreateDirectory(PluginsDirectory);
ApplyPendingPluginDeletions();
UnloadInstalledPlugins();
AppLogger.Info("PluginRuntime", $"Loading installed plugins from '{PluginsDirectory}'.");
var disabledPluginIds = GetDisabledPluginIds();
var settingsSnapshot = _appSettingsService.Load();
@@ -81,6 +82,9 @@ public sealed class PluginRuntimeService : IDisposable
var discoveryFailures = new List<PluginLoadResult>();
var candidates = DiscoverCandidates(discoveryFailures);
_loadResults.AddRange(discoveryFailures);
AppLogger.Info(
"PluginRuntime",
$"Plugin discovery completed. Candidates={candidates.Count}; DiscoveryFailures={discoveryFailures.Count}; PluginsDirectory='{PluginsDirectory}'.");
var selectedPluginIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var candidate in candidates)
@@ -93,6 +97,7 @@ public sealed class PluginRuntimeService : IDisposable
new InvalidOperationException(
$"Duplicate plugin id '{candidate.Manifest.Id}' was found. Source '{candidate.SourcePath}' was ignored because a higher-priority source was already selected."));
_loadResults.Add(duplicateFailure);
LogPluginFailure("CatalogSelection", duplicateFailure, treatAsError: false);
continue;
}
@@ -113,7 +118,13 @@ public sealed class PluginRuntimeService : IDisposable
try
{
AppLogger.Info(
"PluginRuntime",
$"Preparing shared contracts. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'; SourceKind='{candidate.SourceKind}'.");
RegisterSharedContractsForLoad(candidate.Manifest);
AppLogger.Info(
"PluginRuntime",
$"Shared contracts ready. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'.");
}
catch (Exception ex)
{
@@ -128,10 +139,13 @@ public sealed class PluginRuntimeService : IDisposable
ex.Message,
0,
0));
Debug.WriteLine($"[PluginRuntime] Failed to prepare dependencies for '{candidate.Manifest.Id}': {ex}");
LogPluginFailure("DependencyPrepare", dependencyFailure, treatAsError: false);
continue;
}
AppLogger.Info(
"PluginRuntime",
$"Starting plugin load. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'; SourceKind='{candidate.SourceKind}'.");
var loadResult = candidate.SourceKind switch
{
PluginCatalogSourceKind.Package => _loader.LoadFromPackage(
@@ -160,6 +174,9 @@ public sealed class PluginRuntimeService : IDisposable
null,
loadResult.LoadedPlugin.SettingsPages.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}.");
Debug.WriteLine($"[PluginRuntime] Loaded '{loadResult.Manifest?.Id}' from '{loadResult.SourcePath}'.");
continue;
}
@@ -173,11 +190,15 @@ public sealed class PluginRuntimeService : IDisposable
loadResult.Error?.Message,
0,
0));
LogPluginFailure("Load", loadResult, treatAsError: true);
Debug.WriteLine($"[PluginRuntime] Failed to load plugin from '{loadResult.SourcePath}': {loadResult.Error}");
}
if (_catalog.Count == 0 && discoveryFailures.Count == 0)
{
AppLogger.Info(
"PluginRuntime",
$"No plugin packages or loose manifests were discovered under '{PluginsDirectory}'.");
Debug.WriteLine($"[PluginRuntime] No .laapp packages or loose plugin manifests found under '{PluginsDirectory}'.");
}
}
@@ -392,7 +413,9 @@ public sealed class PluginRuntimeService : IDisposable
}
catch (Exception ex)
{
failures.Add(PluginLoadResult.Failure(packagePath, null, ex));
var failure = PluginLoadResult.Failure(packagePath, null, ex);
failures.Add(failure);
LogPluginFailure("ManifestValidation", failure, treatAsError: false);
}
}
@@ -405,7 +428,9 @@ public sealed class PluginRuntimeService : IDisposable
}
catch (Exception ex)
{
failures.Add(PluginLoadResult.Failure(manifestPath, null, ex));
var failure = PluginLoadResult.Failure(manifestPath, null, ex);
failures.Add(failure);
LogPluginFailure("ManifestValidation", failure, treatAsError: false);
}
}
@@ -717,6 +742,21 @@ public sealed class PluginRuntimeService : IDisposable
return Path.Combine(PluginsDirectory, PendingDeletionFileName);
}
private static void LogPluginFailure(string stage, PluginLoadResult result, bool treatAsError)
{
var manifest = result.Manifest;
var message =
$"Plugin load issue. Stage='{stage}'; PluginId='{manifest?.Id ?? "<unknown>"}'; SourcePath='{result.SourcePath}'; ManifestVersion='{manifest?.Version ?? "<unknown>"}'; ApiVersion='{manifest?.ApiVersion ?? "<unknown>"}'; Error='{result.Error?.Message ?? "<none>"}'.";
if (treatAsError)
{
AppLogger.Error("PluginRuntime", message, result.Error);
return;
}
AppLogger.Warn("PluginRuntime", message, result.Error);
}
private void RemovePluginFromSnapshot(string pluginId)
{
var snapshot = _appSettingsService.Load();

View File

@@ -7,31 +7,40 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.PluginSettingsPage">
<StackPanel x:Name="PluginSettingsPanel" Spacing="16">
<StackPanel x:Name="PluginSettingsPanel"
MaxWidth="920"
Spacing="16">
<TextBlock x:Name="PluginSettingsPanelTitleTextBlock"
IsVisible="False"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Plugins" />
<TextBlock x:Name="PluginSettingsPanelSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Manage installed plugins, local package import, and runtime availability from one place." />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="PluginSystemSettingsExpander"
Header="Plugin Runtime"
Description="Manage plugin loading and backend isolation."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource Glyph="&#xe734;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
<fi:SymbolIconSource Symbol="PuzzlePiece"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="PluginSystemDescriptionTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="This page will host installed plugin management, permission review, and sandboxed backend runtime controls." />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
<Border Background="{DynamicResource LayerFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Padding="14">
<TextBlock x:Name="PluginSystemStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Plugin management UI is not connected yet. Next step is wiring the loader, permissions, and worker isolation state into this panel." />
</Border>
@@ -47,16 +56,17 @@
Description="Manage installed plugins here."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource Glyph="&#xe8fd;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
<fi:SymbolIconSource Symbol="Apps"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="PluginRestartHintTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Plugin enable state changes take effect after restarting the app." />
<TextBlock x:Name="PluginCatalogEmptyTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="No plugins found."
IsVisible="False" />
</StackPanel>
@@ -70,7 +80,8 @@
Description="Open a .laapp package and stage it into the local plugin directory."
IsExpanded="False">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource Glyph="&#xe8b7;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
<fi:SymbolIconSource Symbol="ArrowUpload"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
@@ -79,11 +90,11 @@
Click="OnInstallPluginPackageClick"
Content="Open .laapp package" />
<TextBlock x:Name="PluginPackageImportHintTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Open a .laapp package to install it into the local plugin directory." />
<TextBlock x:Name="PluginPackageImportStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
IsVisible="False" />
</StackPanel>

View File

@@ -8,6 +8,7 @@ using System.Runtime.Loader;
using System.Security.Cryptography;
using System.Threading;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Plugins;
@@ -48,6 +49,9 @@ internal sealed class PluginSharedContractManager : IDisposable
}
var document = LoadIndex(cancellationToken);
AppLogger.Info(
"PluginSharedContracts",
$"Shared contract index loaded for plugin '{manifest.Id}'. SourceContracts={document.Contracts.Count}.");
foreach (var reference in manifest.SharedContracts)
{
EnsureInstalled(document, reference, cancellationToken);
@@ -64,9 +68,19 @@ internal sealed class PluginSharedContractManager : IDisposable
}
var assemblyNames = new List<string>(manifest.SharedContracts.Count);
AirAppMarketIndexDocument? document = null;
foreach (var reference in manifest.SharedContracts)
{
var assemblyPath = GetInstalledAssemblyPath(reference);
if (!File.Exists(assemblyPath))
{
document ??= LoadIndex(cancellationToken);
AppLogger.Info(
"PluginSharedContracts",
$"Installing missing shared contract during plugin load. PluginId='{manifest.Id}'; ContractId='{reference.Id}'; Version='{reference.Version}'; Destination='{assemblyPath}'.");
EnsureInstalled(document, reference, cancellationToken);
}
if (!File.Exists(assemblyPath))
{
throw new InvalidOperationException(
@@ -115,10 +129,12 @@ internal sealed class PluginSharedContractManager : IDisposable
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
var temporaryPath = destinationPath + ".download";
var resolvedSource = entry.DownloadUrl;
try
{
if (AirAppMarketDefaults.TryResolveWorkspaceFile(entry.DownloadUrl, out var localSourcePath))
{
resolvedSource = localSourcePath;
File.Copy(localSourcePath, temporaryPath, overwrite: true);
}
else
@@ -136,6 +152,9 @@ internal sealed class PluginSharedContractManager : IDisposable
ValidateInstalledFile(temporaryPath, entry);
File.Move(temporaryPath, destinationPath, overwrite: true);
AppLogger.Info(
"PluginSharedContracts",
$"Installed shared contract. ContractId='{reference.Id}'; Version='{reference.Version}'; Source='{resolvedSource}'; Destination='{destinationPath}'.");
}
finally
{
@@ -145,6 +164,7 @@ internal sealed class PluginSharedContractManager : IDisposable
private AirAppMarketIndexDocument LoadIndex(CancellationToken cancellationToken)
{
AppLogger.Info("PluginSharedContracts", "Loading market index for shared contract resolution.");
var result = _indexService.LoadAsync(cancellationToken).GetAwaiter().GetResult();
if (!result.Success || result.Document is null)
{
@@ -152,6 +172,10 @@ internal sealed class PluginSharedContractManager : IDisposable
$"Failed to load market index for shared contract resolution: {result.ErrorMessage ?? "Unknown error"}");
}
AppLogger.Info(
"PluginSharedContracts",
$"Market index ready. Source='{result.Source}'; Location='{result.SourceLocation}'; Warning='{result.WarningMessage ?? string.Empty}'.");
return result.Document;
}

View File

@@ -3,9 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using FluentIcons.Common;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
@@ -17,11 +17,12 @@ public partial class SettingsWindow
private void InitializePluginSettingsNavigation()
{
if (_pluginSettingsPageHosts.Count > 0)
{
return;
}
_pluginSettingsPageHosts.Clear();
_pluginSettingsNavItems.Clear();
}
private void RegisterPluginSettingsDefinitions()
{
var runtime = (Application.Current as App)?.PluginRuntimeService;
var contributions = runtime?.SettingsPages
.OrderBy(contribution => contribution.Registration.SortOrder)
@@ -31,7 +32,6 @@ public partial class SettingsWindow
if (contributions is not { Length: > 0 })
{
SettingsPluginNavSection.IsVisible = false;
return;
}
@@ -39,23 +39,21 @@ public partial class SettingsWindow
.GroupBy(contribution => contribution.Plugin.Manifest.Id, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
foreach (var contribution in contributions)
for (var i = 0; i < contributions.Length; i++)
{
var contribution = contributions[i];
var tag = BuildPluginSettingsTag(contribution);
var navigationTitle = BuildPluginSettingsNavigationTitle(contribution, pageCountsByPluginId);
var navItem = CreateSettingsNavItem(tag, Symbol.PuzzlePiece, navigationTitle);
ToolTip.SetTip(navItem, $"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}");
_pluginSettingsPageHosts[tag] = CreatePluginSettingsPageHost(contribution);
SettingsPluginNavHost.Children.Add(navItem);
_pluginSettingsNavItems[tag] = navItem;
var pageHost = CreatePluginSettingsPageHost(contribution);
pageHost.IsVisible = false;
SettingsContentPagesHost.Children.Add(pageHost);
_pluginSettingsPageHosts[tag] = pageHost;
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
tag,
BuildPluginSettingsNavigationTitle(contribution, pageCountsByPluginId),
BuildPluginSettingsPageDescription(contribution),
FluentIcons.Common.Symbol.PuzzlePiece,
IndependentSettingsPageCategory.External,
200 + i,
$"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}"));
}
SettingsPluginNavSection.IsVisible = SettingsPluginNavHost.Children.Count > 0;
}
private static string BuildPluginSettingsTag(PluginSettingsPageContribution contribution)
@@ -72,6 +70,15 @@ public partial class SettingsWindow
: 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;
@@ -87,6 +94,7 @@ public partial class SettingsWindow
return new StackPanel
{
Spacing = 16,
MaxWidth = 920,
Children =
{
new TextBlock
@@ -94,12 +102,12 @@ public partial class SettingsWindow
Text = contribution.Registration.Title,
FontSize = 24,
FontWeight = FontWeight.SemiBold,
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush")
Foreground = GetThemeBrush("TextFillColorPrimaryBrush")
},
new TextBlock
{
Text = contribution.Plugin.Manifest.Name,
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush")
Foreground = GetThemeBrush("TextFillColorSecondaryBrush")
},
content
}
@@ -123,58 +131,32 @@ public partial class SettingsWindow
};
}
private void UpdatePluginSettingsPageVisibility(string? selectedTag)
{
foreach (var pair in _pluginSettingsPageHosts)
{
pair.Value.IsVisible = string.Equals(pair.Key, selectedTag, StringComparison.OrdinalIgnoreCase);
}
}
internal void RefreshPluginSettingsNavigation()
{
foreach (var pair in _pluginSettingsPageHosts.ToArray())
{
if (_pluginSettingsNavItems.TryGetValue(pair.Key, out var navItem))
{
SettingsPluginNavHost.Children.Remove(navItem);
}
SettingsContentPagesHost.Children.Remove(pair.Value);
}
_pluginSettingsPageHosts.Clear();
_pluginSettingsNavItems.Clear();
SettingsPluginNavSection.IsVisible = false;
InitializePluginSettingsNavigation();
if (GetSettingsNavItem(_selectedSettingsTabTag) is null)
{
SelectSettingsTab("Plugins", persistSelection: false);
}
else
{
SelectSettingsTab(_selectedSettingsTabTag, persistSelection: false);
}
var preferredTag = NormalizeSettingsPageTag(_selectedSettingsTabTag);
InitializeSettingsNavigation();
SelectSettingsTab(
_settingsPageDefinitions.ContainsKey(preferredTag) ? preferredTag : "Plugins",
persistSelection: false);
PluginSettingsPanel?.RefreshFromRuntime();
}
private string? GetSelectedSettingsTabTag()
{
return _selectedSettingsTabTag;
return NormalizeSettingsPageTag(_selectedSettingsTabTag);
}
private int ResolveSelectedSettingsTabIndex()
{
var selectedTag = GetSelectedSettingsTabTag();
if (string.IsNullOrWhiteSpace(selectedTag))
if (SettingsNavView?.MenuItems is null)
{
return 0;
}
var buttons = EnumerateSettingsNavItems().ToList();
for (var i = 0; i < buttons.Count; i++)
var items = SettingsNavView.MenuItems.OfType<NavigationViewItem>().ToList();
for (var i = 0; i < items.Count; i++)
{
if (string.Equals(buttons[i].Tag?.ToString(), selectedTag, StringComparison.OrdinalIgnoreCase))
if (string.Equals(items[i].Tag?.ToString(), NormalizeSettingsPageTag(_selectedSettingsTabTag), StringComparison.OrdinalIgnoreCase))
{
return i;
}
@@ -185,21 +167,32 @@ public partial class SettingsWindow
private void RestoreSettingsTabSelection(AppSettingsSnapshot snapshot)
{
var buttons = EnumerateSettingsNavItems().ToList();
if (buttons.Count == 0)
if (SettingsNavView?.MenuItems is null || SettingsNavView.MenuItems.Count == 0)
{
return;
}
if (!string.IsNullOrWhiteSpace(snapshot.SettingsTabTag) &&
GetSettingsNavItem(snapshot.SettingsTabTag) is not null)
var items = SettingsNavView.MenuItems.OfType<NavigationViewItem>().ToList();
if (items.Count == 0)
{
SelectSettingsTab(snapshot.SettingsTabTag, persistSelection: false);
return;
}
var safeIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, Math.Max(0, buttons.Count - 1));
var button = buttons[safeIndex];
SelectSettingsTab(button.Tag?.ToString() ?? "Wallpaper", persistSelection: false);
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];
}
}