setting_re3

This commit is contained in:
lincube
2026-03-13 09:10:00 +08:00
parent c4df243610
commit 3b3f060f33
70 changed files with 1986 additions and 8966 deletions

View File

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

View File

@@ -1,15 +0,0 @@
using Avalonia.Controls;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
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,74 +0,0 @@
using System;
using System.Linq;
using Avalonia.Controls;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Models;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private void InitializePluginSettingsNavigation()
{
// Legacy plugin settings pages are removed in API-only settings mode.
}
private void UpdatePluginSettingsPageVisibility(string? selectedTag)
{
_ = selectedTag;
}
internal void RefreshPluginSettingsNavigation()
{
// Legacy plugin settings pages are removed in API-only settings mode.
}
private string? GetSelectedSettingsTabTag()
{
return (SettingsNavView?.SelectedItem as NavigationViewItem)?.Tag?.ToString();
}
private int ResolveSelectedSettingsTabIndex()
{
if (SettingsNavView?.SelectedItem is null || SettingsNavView.MenuItems is null)
{
return 0;
}
for (var i = 0; i < SettingsNavView.MenuItems.Count; i++)
{
if (ReferenceEquals(SettingsNavView.MenuItems[i], SettingsNavView.SelectedItem))
{
return i;
}
}
return 0;
}
private void RestoreSettingsTabSelection(AppSettingsSnapshot snapshot)
{
if (SettingsNavView?.MenuItems is null || SettingsNavView.MenuItems.Count == 0)
{
return;
}
if (!string.IsNullOrWhiteSpace(snapshot.SettingsTabTag))
{
var taggedItem = SettingsNavView.MenuItems
.OfType<NavigationViewItem>()
.FirstOrDefault(item => string.Equals(item.Tag?.ToString(), snapshot.SettingsTabTag, StringComparison.OrdinalIgnoreCase));
if (taggedItem is not null)
{
SettingsNavView.SelectedItem = taggedItem;
return;
}
}
var safeIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, Math.Max(0, SettingsNavView.MenuItems.Count - 1));
if (SettingsNavView.MenuItems[safeIndex] is NavigationViewItem navItem)
{
SettingsNavView.SelectedItem = navItem;
}
}
}

View File

@@ -1,32 +0,0 @@
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
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();
}
}

View File

@@ -1,43 +0,0 @@
<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 TextFillColorPrimaryBrush}"
Text="Plugin Market" />
<TextBlock x:Name="PluginMarketPanelSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Browse plugins from the official LanAirApp source, review package details, and stage installations safely." />
<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

@@ -1,72 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.PluginMarket;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginMarketSettingsPage : UserControl
{
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
private PluginMarketEmbeddedView? _pluginMarketView;
public PluginMarketSettingsPage()
{
InitializeComponent();
AttachedToVisualTree += (_, _) => RefreshFromRuntime();
}
public void RefreshFromRuntime()
{
PluginMarketPanelTitleTextBlock.Text = L("settings.plugin_market.title", "Plugin Market");
PluginMarketPanelSubtitleTextBlock.Text = L(
"settings.plugin_market.subtitle",
"Browse plugins from the official LanAirApp source and stage installs.");
var runtime = (Application.Current as App)?.PluginRuntimeService;
if (runtime is null)
{
PluginMarketContentHost.Content = CreateUnavailableState();
return;
}
if (_pluginMarketView is null)
{
_pluginMarketView = new PluginMarketEmbeddedView(runtime);
}
_pluginMarketView.RefreshLocalization();
_pluginMarketView.RefreshInstalledSnapshot();
if (!ReferenceEquals(PluginMarketContentHost.Content, _pluginMarketView))
{
PluginMarketContentHost.Content = _pluginMarketView;
}
}
private Control CreateUnavailableState()
{
return new Border
{
Background = new SolidColorBrush(Color.Parse("#14000000")),
CornerRadius = new CornerRadius(16),
Padding = new Thickness(16),
Child = new TextBlock
{
Text = L(
"settings.plugin_market.unavailable",
"Plugin runtime is not available, so the official market cannot be opened right now."),
TextWrapping = TextWrapping.Wrap,
Foreground = PluginMarketPanelSubtitleTextBlock.Foreground
}
};
}
private string L(string key, string fallback)
{
var snapshot = _appSettingsService.Load();
return _localizationService.GetString(snapshot.LanguageCode, key, fallback);
}
}

View File

@@ -24,13 +24,12 @@ public sealed class PluginRuntimeService : IDisposable
private readonly PluginLoaderOptions _loaderOptions;
private readonly PluginLoader _loader;
private readonly AppSettingsService _appSettingsService = new();
private readonly IHostApplicationLifecycle _applicationLifecycle = new HostApplicationLifecycleService();
private readonly PluginExportRegistry _exportRegistry = new();
private readonly PluginSharedContractManager _sharedContractManager;
private readonly IServiceProvider _hostServices;
private readonly IPluginPackageManager _packageManager;
private readonly SettingsFacadeService _settingsFacade;
private readonly ISettingsFacadeService _settingsFacade;
private readonly SettingsCatalogService _settingsCatalogService;
private readonly List<LoadedPlugin> _loadedPlugins = [];
private readonly List<PluginLoadResult> _loadResults = [];
@@ -39,14 +38,19 @@ public sealed class PluginRuntimeService : IDisposable
private readonly List<PluginDesktopComponentContribution> _desktopComponents = [];
private readonly object _packageMutationGate = new();
public PluginRuntimeService()
public PluginRuntimeService(ISettingsFacadeService? settingsFacade = null)
{
PluginsDirectory = Path.Combine(AppContext.BaseDirectory, "Extensions", "Plugins");
_sharedContractManager = new PluginSharedContractManager(
Path.Combine(GetUserDataRootDirectory(), "PluginMarket"));
_packageManager = new PluginRuntimePackageManager(this);
_settingsFacade = new SettingsFacadeService(this);
_settingsCatalogService = (SettingsCatalogService)_settingsFacade.Catalog;
_settingsFacade = settingsFacade ?? new SettingsFacadeService();
_settingsCatalogService = _settingsFacade.Catalog as SettingsCatalogService
?? new SettingsCatalogService();
if (_settingsFacade is SettingsFacadeService concreteFacade)
{
concreteFacade.BindPluginRuntime(this);
}
_hostServices = new PluginHostServiceProvider(
_packageManager,
_applicationLifecycle,
@@ -81,7 +85,7 @@ public sealed class PluginRuntimeService : IDisposable
AppLogger.Info("PluginRuntime", $"Loading installed plugins from '{PluginsDirectory}'.");
var disabledPluginIds = GetDisabledPluginIds();
var settingsSnapshot = _appSettingsService.Load();
var settingsSnapshot = LoadAppSettingsSnapshot();
var hostLanguageCode = PluginLocalizer.NormalizeLanguageCode(settingsSnapshot.LanguageCode);
var hostProperties = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
{
@@ -222,7 +226,7 @@ public sealed class PluginRuntimeService : IDisposable
return false;
}
var snapshot = _appSettingsService.Load();
var snapshot = LoadAppSettingsSnapshot();
var disabledPluginIds = snapshot.DisabledPluginIds is { Count: > 0 }
? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase)
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -239,7 +243,7 @@ public sealed class PluginRuntimeService : IDisposable
snapshot.DisabledPluginIds = disabledPluginIds
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
.ToList();
_appSettingsService.Save(snapshot);
SaveAppSettingsSnapshot(snapshot);
PendingRestartStateService.SetPending(PendingRestartStateService.PluginCatalogReason, true);
for (var i = 0; i < _catalog.Count; i++)
@@ -386,7 +390,10 @@ public sealed class PluginRuntimeService : IDisposable
{
UnloadInstalledPlugins();
_sharedContractManager.Dispose();
_settingsFacade.Dispose();
if (_settingsFacade is IDisposable disposable && !ReferenceEquals(_settingsFacade, HostSettingsFacadeProvider.GetOrCreate()))
{
disposable.Dispose();
}
}
private void UnloadInstalledPlugins()
@@ -409,7 +416,7 @@ public sealed class PluginRuntimeService : IDisposable
private HashSet<string> GetDisabledPluginIds()
{
var snapshot = _appSettingsService.Load();
var snapshot = LoadAppSettingsSnapshot();
return snapshot.DisabledPluginIds is { Count: > 0 }
? new HashSet<string>(snapshot.DisabledPluginIds, StringComparer.OrdinalIgnoreCase)
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -781,13 +788,23 @@ public sealed class PluginRuntimeService : IDisposable
private void RemovePluginFromSnapshot(string pluginId)
{
var snapshot = _appSettingsService.Load();
var snapshot = LoadAppSettingsSnapshot();
if (snapshot.DisabledPluginIds.RemoveAll(id => string.Equals(id, pluginId, StringComparison.OrdinalIgnoreCase)) > 0)
{
_appSettingsService.Save(snapshot);
SaveAppSettingsSnapshot(snapshot);
}
}
private AppSettingsSnapshot LoadAppSettingsSnapshot()
{
return _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
}
private void SaveAppSettingsSnapshot(AppSettingsSnapshot snapshot)
{
_settingsFacade.Settings.SaveSnapshot(SettingsScope.App, snapshot);
}
private void RemovePluginFromCatalog(string pluginId)
{
_catalog.RemoveAll(entry => string.Equals(entry.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));

View File

@@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services;
internal sealed class PluginScopedSettingsService : IPluginSettingsService
{
private readonly ISettingsService _settingsService;
public PluginScopedSettingsService(string pluginId, ISettingsService settingsService)
{
PluginId = string.IsNullOrWhiteSpace(pluginId) ? "__unknown__" : pluginId.Trim();
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public string PluginId { get; }
public IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId)
{
return new ScopedComponentAccessor(this, _settingsService.GetComponentAccessor(componentId, placementId));
}
public T LoadComponentSection<T>(string componentId, string? placementId, string sectionId) where T : new()
{
return _settingsService.LoadSection<T>(
SettingsScope.ComponentInstance,
componentId,
BuildScopedSectionId(sectionId),
placementId);
}
public void SaveComponentSection<T>(
string componentId,
string? placementId,
string sectionId,
T section,
IReadOnlyCollection<string>? changedKeys = null)
{
_settingsService.SaveSection(
SettingsScope.ComponentInstance,
componentId,
BuildScopedSectionId(sectionId),
section,
placementId,
changedKeys);
}
public void DeleteComponentSection(string componentId, string? placementId, string sectionId)
{
_settingsService.DeleteSection(
SettingsScope.ComponentInstance,
componentId,
BuildScopedSectionId(sectionId),
placementId);
}
private string BuildScopedSectionId(string sectionId)
{
var normalizedSectionId = string.IsNullOrWhiteSpace(sectionId) ? "__default__" : sectionId.Trim();
return $"{PluginId}:{normalizedSectionId}";
}
private sealed class ScopedComponentAccessor : IComponentSettingsAccessor
{
private readonly PluginScopedSettingsService _owner;
private readonly IComponentSettingsAccessor _inner;
public ScopedComponentAccessor(PluginScopedSettingsService owner, IComponentSettingsAccessor inner)
{
_owner = owner;
_inner = inner;
}
public string ComponentId => _inner.ComponentId;
public string? PlacementId => _inner.PlacementId;
public T LoadSnapshot<T>() where T : new()
{
return _inner.LoadSnapshot<T>();
}
public void SaveSnapshot<T>(T snapshot, IReadOnlyCollection<string>? changedKeys = null)
{
_inner.SaveSnapshot(snapshot, changedKeys);
}
public T LoadSection<T>(string sectionId) where T : new()
{
return _inner.LoadSection<T>(_owner.BuildScopedSectionId(sectionId));
}
public void SaveSection<T>(string sectionId, T section, IReadOnlyCollection<string>? changedKeys = null)
{
_inner.SaveSection(_owner.BuildScopedSectionId(sectionId), section, changedKeys);
}
public void DeleteSection(string sectionId)
{
_inner.DeleteSection(_owner.BuildScopedSectionId(sectionId));
}
public T? GetValue<T>(string key)
{
return _inner.GetValue<T>($"{_owner.PluginId}:{key}");
}
public void SetValue<T>(string key, T value, IReadOnlyCollection<string>? changedKeys = null)
{
_inner.SetValue($"{_owner.PluginId}:{key}", value, changedKeys);
}
}
}

View File

@@ -1,460 +0,0 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginSettingsPage : UserControl
{
private static readonly IBrush SuccessBrush = new SolidColorBrush(Color.Parse("#FF0F766E"));
private static readonly IBrush ErrorBrush = new SolidColorBrush(Color.Parse("#FFC42B1C"));
private static readonly IBrush DestructiveBrush = new SolidColorBrush(Color.Parse("#FFF87171"));
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
private string? _packageImportStatusMessage;
private bool _packageImportStatusIsError;
public PluginSettingsPage()
{
InitializeComponent();
AttachedToVisualTree += (_, _) => RefreshFromRuntime();
}
public void RefreshFromRuntime()
{
var runtime = (Application.Current as App)?.PluginRuntimeService;
UpdateInstallerUi(runtime);
if (runtime is null)
{
PluginSystemStatusTextBlock.Text = L("settings.plugins.runtime_unavailable", "Plugin runtime is not available.");
PluginRuntimeSummaryPanel.Children.Clear();
InstalledPluginsSettingsExpander.Items.Clear();
PluginRestartHintTextBlock.IsVisible = false;
PluginCatalogEmptyTextBlock.IsVisible = false;
return;
}
BuildRuntimeSummary(runtime);
BuildPluginCatalog(runtime);
}
private void UpdateInstallerUi(PluginRuntimeService? runtime)
{
InstallPluginPackageButton.Content = L("settings.plugins.install_button", "Open .laapp package");
InstallPluginPackageButton.IsEnabled = runtime is not null;
PluginPackageImportHintTextBlock.Text = runtime is null
? L(
"settings.plugins.install_unavailable",
"Plugin runtime is unavailable, so .laapp packages cannot be installed right now.")
: F(
"settings.plugins.install_hint_format",
"Open a .laapp package to install it into: {0}",
runtime.PluginsDirectory);
PluginPackageImportStatusTextBlock.IsVisible = !string.IsNullOrWhiteSpace(_packageImportStatusMessage);
PluginPackageImportStatusTextBlock.Text = _packageImportStatusMessage ?? string.Empty;
PluginPackageImportStatusTextBlock.Foreground = _packageImportStatusIsError ? ErrorBrush : SuccessBrush;
}
private void BuildRuntimeSummary(PluginRuntimeService runtime)
{
var failures = runtime.LoadResults.Where(result => !result.IsSuccess).ToArray();
var enabledCount = runtime.Catalog.Count(entry => entry.IsEnabled);
PluginSystemStatusTextBlock.Text = F(
"settings.plugins.summary_format",
"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.SettingsSections.Count,
runtime.DesktopComponents.Count,
failures.Length);
PluginRuntimeSummaryPanel.Children.Clear();
foreach (var plugin in runtime.Catalog.OrderBy(entry => entry.Manifest.Name, StringComparer.OrdinalIgnoreCase))
{
var status = plugin.IsEnabled
? plugin.IsLoaded
? L("settings.plugins.state.enabled", "Enabled")
: L("settings.plugins.state.enabled_failed", "Enabled / failed to load")
: L("settings.plugins.state.disabled", "Disabled");
PluginRuntimeSummaryPanel.Children.Add(CreateSummaryLine(
F(
"settings.plugins.summary_item_format",
"{0} v{1} | {2}",
plugin.Manifest.Name,
plugin.Manifest.Version ?? "dev",
status)));
}
}
private void BuildPluginCatalog(PluginRuntimeService runtime)
{
InstalledPluginsSettingsExpander.Items.Clear();
var plugins = runtime.Catalog
.OrderBy(entry => entry.Manifest.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
PluginCatalogEmptyTextBlock.IsVisible = plugins.Count == 0;
PluginRestartHintTextBlock.IsVisible = plugins.Count > 0;
foreach (var plugin in plugins)
{
InstalledPluginsSettingsExpander.Items.Add(CreatePluginCatalogItem(runtime, plugin));
}
}
private SettingsExpanderItem CreatePluginCatalogItem(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
return new SettingsExpanderItem
{
Content = entry.Manifest.Name,
Description = BuildPluginSubtitle(entry),
IconSource = CreatePluginCatalogIconSource(),
IsClickEnabled = false,
Footer = CreatePluginCatalogActions(runtime, entry)
};
}
private void OnInstallPluginPackageClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
UiExceptionGuard.FireAndForgetGuarded(
OnInstallPluginPackageAsync,
"PluginSettings.InstallPackage",
context: "Page=PluginSettings",
onHandledException: ex =>
{
SetPackageImportStatus(
F(
"settings.plugins.install_failed_format",
"Failed to install plugin package: {0}",
ex.Message),
isError: true);
return Task.CompletedTask;
});
}
private async Task OnInstallPluginPackageAsync()
{
var runtime = (Application.Current as App)?.PluginRuntimeService;
if (runtime is null)
{
SetPackageImportStatus(
L(
"settings.plugins.install_unavailable",
"Plugin runtime is unavailable, so .laapp packages cannot be installed right now."),
isError: true);
return;
}
var topLevel = TopLevel.GetTopLevel(this);
var storageProvider = topLevel?.StorageProvider;
if (storageProvider is null)
{
SetPackageImportStatus(
L("settings.plugins.install_picker_unavailable", "Storage provider is unavailable."),
isError: true);
return;
}
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = L("settings.plugins.install_picker_title", "Select plugin package"),
AllowMultiple = false,
FileTypeFilter =
[
new FilePickerFileType(L("settings.plugins.install_file_type", ".laapp plugin package"))
{
Patterns = [$"*{PluginSdkInfo.PackageFileExtension}"]
}
]
});
if (files.Count == 0)
{
return;
}
string? temporaryPackagePath = null;
try
{
temporaryPackagePath = await CopyPackageToTemporaryFileAsync(files[0]);
if (string.IsNullOrWhiteSpace(temporaryPackagePath))
{
SetPackageImportStatus(
L("settings.plugins.install_copy_failed", "Failed to copy the selected .laapp package."),
isError: true);
return;
}
var manifest = runtime.InstallPluginPackage(temporaryPackagePath);
RefreshFromRuntime();
RefreshPluginNavigation(TopLevel.GetTopLevel(this));
SetPackageImportStatus(
F(
"settings.plugins.install_success_format",
"Installed plugin '{0}'. Restart the app to apply newly added settings pages and widgets.",
manifest.Name),
isError: false);
}
catch (Exception ex)
{
SetPackageImportStatus(
F(
"settings.plugins.install_failed_format",
"Failed to install plugin package: {0}",
ex.Message),
isError: true);
}
finally
{
if (!string.IsNullOrWhiteSpace(temporaryPackagePath))
{
try
{
File.Delete(temporaryPackagePath);
}
catch
{
// Ignore temporary file cleanup errors.
}
}
}
}
private void OnDeletePluginClick(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
try
{
if (!runtime.DeleteInstalledPlugin(entry.Manifest.Id))
{
SetPackageImportStatus(
F(
"settings.plugins.delete_failed_format",
"Failed to delete plugin: {0}",
entry.Manifest.Name),
isError: true);
return;
}
RefreshFromRuntime();
RefreshPluginNavigation(TopLevel.GetTopLevel(this));
PluginSystemStatusTextBlock.Text = F(
"settings.plugins.delete_success_format",
"Plugin '{0}' was staged for deletion. Restart the app to finish removing it.",
entry.Manifest.Name);
SetPackageImportStatus(
F(
"settings.plugins.delete_success_format",
"Plugin '{0}' was staged for deletion. Restart the app to finish removing it.",
entry.Manifest.Name),
isError: false);
}
catch (Exception ex)
{
SetPackageImportStatus(
F(
"settings.plugins.delete_failed_detail_format",
"Failed to delete plugin '{0}': {1}",
entry.Manifest.Name,
ex.Message),
isError: true);
}
}
private void OnPluginEnabledChanged(PluginRuntimeService runtime, PluginCatalogEntry entry, bool isEnabled)
{
try
{
if (!runtime.SetPluginEnabled(entry.Manifest.Id, isEnabled))
{
return;
}
RefreshFromRuntime();
var toggleState = isEnabled
? L("settings.plugins.toggle_state_enabled", "enabled")
: L("settings.plugins.toggle_state_disabled", "disabled");
SetPackageImportStatus(
F(
"settings.plugins.toggle_result_format",
"Plugin '{0}' was {1} for the next launch. Restart the app to apply page and widget changes.",
entry.Manifest.Name,
toggleState),
isError: false);
}
catch (Exception ex)
{
SetPackageImportStatus(
F(
"settings.plugins.toggle_failed_detail_format",
"Failed to update plugin '{0}': {1}",
entry.Manifest.Name,
ex.Message),
isError: true);
}
}
private void RefreshPluginNavigation(TopLevel? topLevel)
{
switch (topLevel)
{
case MainWindow mainWindow:
mainWindow.RefreshPluginSettingsNavigation();
break;
}
}
private void SetPackageImportStatus(string message, bool isError)
{
_packageImportStatusMessage = string.IsNullOrWhiteSpace(message) ? null : message;
_packageImportStatusIsError = isError;
UpdateInstallerUi((Application.Current as App)?.PluginRuntimeService);
}
private string BuildPluginSubtitle(PluginCatalogEntry entry)
{
var publisher = string.IsNullOrWhiteSpace(entry.Manifest.Author)
? L("settings.plugins.publisher_unknown", "Unknown publisher")
: entry.Manifest.Author;
return F(
"settings.plugins.publisher_format",
"Publisher: {0}",
publisher);
}
private TextBlock CreateSummaryLine(string text)
{
return new TextBlock
{
Text = text,
TextWrapping = TextWrapping.Wrap,
Foreground = PluginSystemDescriptionTextBlock.Foreground
};
}
private string L(string key, string fallback)
{
var snapshot = _appSettingsService.Load();
return _localizationService.GetString(snapshot.LanguageCode, key, fallback);
}
private string F(string key, string fallback, params object[] args)
{
return string.Format(CultureInfo.CurrentCulture, L(key, fallback), args);
}
private FluentIcons.Avalonia.Fluent.SymbolIconSource CreatePluginCatalogIconSource()
{
return new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = FluentIcons.Common.Symbol.PuzzlePiece,
IconVariant = FluentIcons.Common.IconVariant.Regular
};
}
private Control CreatePluginCatalogActions(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
return new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 10,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
Children =
{
CreateEnablePluginToggle(runtime, entry),
CreateDeletePluginButton(runtime, entry)
}
};
}
private ToggleSwitch CreateEnablePluginToggle(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
var toggle = new ToggleSwitch
{
IsChecked = entry.IsEnabled,
VerticalAlignment = VerticalAlignment.Center
};
ToolTip.SetTip(
toggle,
entry.IsEnabled
? L("settings.plugins.toggle_off", "Disable")
: L("settings.plugins.toggle_on", "Enable"));
toggle.IsCheckedChanged += (_, _) => OnPluginEnabledChanged(runtime, entry, toggle.IsChecked == true);
return toggle;
}
private Button CreateDeletePluginButton(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
var button = new Button
{
Width = 36,
Height = 36,
Padding = new Thickness(0),
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
Content = new FluentIcons.Avalonia.Fluent.SymbolIcon
{
Symbol = FluentIcons.Common.Symbol.Delete,
IconVariant = FluentIcons.Common.IconVariant.Regular,
FontSize = 18,
Foreground = DestructiveBrush,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
ToolTip.SetTip(button, L("settings.plugins.delete_button", "Delete plugin"));
button.Click += (_, _) => OnDeletePluginClick(runtime, entry);
return button;
}
private static async Task<string?> CopyPackageToTemporaryFileAsync(IStorageFile file)
{
try
{
var extension = Path.GetExtension(file.Name);
if (string.IsNullOrWhiteSpace(extension))
{
extension = PluginSdkInfo.PackageFileExtension;
}
var temporaryDirectory = Path.Combine(
Path.GetTempPath(),
"LanMountainDesktop",
"PluginImports");
Directory.CreateDirectory(temporaryDirectory);
var temporaryPackagePath = Path.Combine(
temporaryDirectory,
$"{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}{extension}");
await using var sourceStream = await file.OpenReadAsync();
await using var destinationStream = File.Create(temporaryPackagePath);
await sourceStream.CopyToAsync(destinationStream);
return temporaryPackagePath;
}
catch
{
return null;
}
}
}

View File

@@ -1,106 +0,0 @@
<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"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.PluginSettingsPage">
<StackPanel x:Name="PluginSettingsPanel"
MaxWidth="920"
Spacing="16">
<TextBlock x:Name="PluginSettingsPanelTitleTextBlock"
IsVisible="False"
FontSize="24"
FontWeight="SemiBold"
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>
<fi:SymbolIconSource Symbol="PuzzlePiece"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="PluginSystemDescriptionTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="This page will host installed plugin management, permission review, and sandboxed backend runtime controls." />
<Border Background="{DynamicResource LayerFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Padding="14">
<TextBlock x:Name="PluginSystemStatusTextBlock"
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>
<StackPanel x:Name="PluginRuntimeSummaryPanel" Spacing="6" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="InstalledPluginsSettingsExpander"
Header="Installed Plugins"
Description="Manage installed plugins here."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Apps"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="PluginRestartHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Plugin enable state changes take effect after restarting the app." />
<TextBlock x:Name="PluginCatalogEmptyTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="No plugins found."
IsVisible="False" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="ImportPluginPackageSettingsExpander"
Header="Install From Package"
Description="Open a .laapp package and stage it into the local plugin directory."
IsExpanded="False">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ArrowUpload"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<Button x:Name="InstallPluginPackageButton"
HorizontalAlignment="Left"
Click="OnInstallPluginPackageClick"
Content="Open .laapp package" />
<TextBlock x:Name="PluginPackageImportHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Open a .laapp package to install it into the local plugin directory." />
<TextBlock x:Name="PluginPackageImportStatusTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
IsVisible="False" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

@@ -26,8 +26,6 @@
- `PluginLoadContext.cs`
- `PluginRuntimeService.cs`
- `PluginCatalogEntry.cs`
- `PluginSettingsPage.axaml`
- `PluginSettingsPage.Host.cs`
- `PluginMarketIndexService.cs`
- `PluginMarketInstallService.cs`