mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
setting_re3
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class MainWindow
|
||||
{
|
||||
private void ApplyPluginMarketSettingsLocalization()
|
||||
{
|
||||
PluginMarketSettingsPanel.RefreshFromRuntime();
|
||||
}
|
||||
}
|
||||
@@ -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")!;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
114
LanMountainDesktop/plugins/PluginScopedSettingsService.cs
Normal file
114
LanMountainDesktop/plugins/PluginScopedSettingsService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -26,8 +26,6 @@
|
||||
- `PluginLoadContext.cs`
|
||||
- `PluginRuntimeService.cs`
|
||||
- `PluginCatalogEntry.cs`
|
||||
- `PluginSettingsPage.axaml`
|
||||
- `PluginSettingsPage.Host.cs`
|
||||
- `PluginMarketIndexService.cs`
|
||||
- `PluginMarketInstallService.cs`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user