mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
setting_re1
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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="" 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="" 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="" 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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user