This commit is contained in:
lincube
2026-03-10 21:25:47 +08:00
parent e1be072b97
commit 5003ff1be2
33 changed files with 2171 additions and 1021 deletions

View File

@@ -9,6 +9,7 @@ public partial class MainWindow
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

@@ -18,10 +18,14 @@ public partial class MainWindow
InstalledPluginsSettingsExpander.Header = L("settings.plugins.installed_header", "Installed Plugins");
InstalledPluginsSettingsExpander.Description = L(
"settings.plugins.installed_desc",
"Enable or disable plugins here. Detailed plugin settings appear as separate settings pages.");
"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 enable state changes take effect after restarting the app.");
"Plugin installation and deletion changes take effect after restarting the app.");
PluginCatalogEmptyTextBlock.Text = L("settings.plugins.empty", "No plugins found.");
PluginSettingsPanel.RefreshFromRuntime();
}

View File

@@ -10,6 +10,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using LanMountainDesktop.Services;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Plugins;
@@ -346,6 +347,9 @@ public sealed class PluginLoader
private string ExtractPackage(string packagePath, string pluginsRootDirectory)
{
var extractionDirectory = GetPackageExtractionDirectory(pluginsRootDirectory, packagePath);
AppLogger.Info(
"PluginLoader",
$"Extracting package '{packagePath}' to '{extractionDirectory}'.");
RecreateDirectory(extractionDirectory);
ZipFile.ExtractToDirectory(packagePath, extractionDirectory, overwriteFiles: true);
return extractionDirectory;
@@ -381,7 +385,7 @@ public sealed class PluginLoader
{
if (Directory.Exists(directoryPath))
{
Directory.Delete(directoryPath, recursive: true);
FileOperationRetryHelper.DeleteDirectoryWithRetry(directoryPath, recursive: true, "PluginLoader");
}
Directory.CreateDirectory(directoryPath);

View File

@@ -61,7 +61,10 @@ internal sealed class PluginMarketEmbeddedView : UserControl, IDisposable
public PluginMarketEmbeddedView(PluginRuntimeService runtime)
{
_runtime = runtime;
var dataDirectory = Path.Combine(AppContext.BaseDirectory, "Data", "AirAppMarket");
var dataDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop",
"AirAppMarket");
_indexService = new AirAppMarketIndexService(new AirAppMarketCacheService(dataDirectory));
_installService = new AirAppMarketInstallService(runtime, dataDirectory);
_readmeService = new AirAppMarketReadmeService();

View File

@@ -44,7 +44,13 @@ internal sealed class AirAppMarketInstallService : IDisposable
try
{
AppLogger.Info(
"PluginMarket",
$"Starting install. PluginId='{plugin.Id}'; Version='{plugin.Version}'; DownloadPath='{downloadPath}'.");
var resolvedDownloadUrl = await _releaseResolverService.ResolveDownloadUrlAsync(plugin, cancellationToken);
AppLogger.Info(
"PluginMarket",
$"Resolved download url for '{plugin.Id}' to '{resolvedDownloadUrl}'.");
if (AirAppMarketDefaults.TryResolveWorkspaceFile(resolvedDownloadUrl, out var localPackagePath))
{
@@ -84,14 +90,24 @@ internal sealed class AirAppMarketInstallService : IDisposable
}
var manifest = _runtime.InstallPluginPackage(downloadPath);
AppLogger.Info(
"PluginMarket",
$"Install staged successfully. PluginId='{manifest.Id}'; InstalledName='{manifest.Name}'; PackagePath='{downloadPath}'.");
return new AirAppMarketInstallResult(true, manifest, null);
}
catch (OperationCanceledException)
{
AppLogger.Warn(
"PluginMarket",
$"Install canceled. PluginId='{plugin.Id}'; Version='{plugin.Version}'; DownloadPath='{downloadPath}'.");
throw;
}
catch (Exception ex)
{
AppLogger.Error(
"PluginMarket",
$"Install failed. PluginId='{plugin.Id}'; Version='{plugin.Version}'; DownloadPath='{downloadPath}'.",
ex);
return new AirAppMarketInstallResult(false, null, ex.Message);
}
}

View File

@@ -5,6 +5,7 @@ using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
@@ -16,6 +17,8 @@ namespace LanMountainDesktop.Services;
public sealed class PluginRuntimeService : IDisposable
{
private const string PendingDeletionFileName = ".pending-plugin-deletions.json";
private readonly PluginLoader _loader;
private readonly AppSettingsService _appSettingsService = new();
private readonly IServiceProvider _hostServices;
@@ -25,6 +28,7 @@ public sealed class PluginRuntimeService : IDisposable
private readonly List<PluginCatalogEntry> _catalog = [];
private readonly List<PluginSettingsPageContribution> _settingsPages = [];
private readonly List<PluginDesktopComponentContribution> _desktopComponents = [];
private readonly object _packageMutationGate = new();
public PluginRuntimeService()
{
@@ -49,6 +53,7 @@ public sealed class PluginRuntimeService : IDisposable
public void LoadInstalledPlugins()
{
Directory.CreateDirectory(PluginsDirectory);
ApplyPendingPluginDeletions();
UnloadInstalledPlugins();
var disabledPluginIds = GetDisabledPluginIds();
@@ -170,6 +175,7 @@ public sealed class PluginRuntimeService : IDisposable
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
.ToList();
_appSettingsService.Save(snapshot);
PendingRestartStateService.SetPending(PendingRestartStateService.PluginCatalogReason, true);
for (var i = 0; i < _catalog.Count; i++)
{
@@ -184,7 +190,50 @@ public sealed class PluginRuntimeService : IDisposable
public PluginManifest InstallPluginPackage(string packagePath)
{
return InstallPluginPackageCore(packagePath).Manifest;
lock (_packageMutationGate)
{
return InstallPluginPackageCore(packagePath).Manifest;
}
}
public bool DeleteInstalledPlugin(string pluginId)
{
lock (_packageMutationGate)
{
return DeleteInstalledPluginCore(pluginId);
}
}
private bool DeleteInstalledPluginCore(string pluginId)
{
if (string.IsNullOrWhiteSpace(pluginId))
{
return false;
}
var entry = _catalog.FirstOrDefault(candidate =>
string.Equals(candidate.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
if (entry is null)
{
return false;
}
var targetPath = ResolvePluginRemovalTargetPath(entry);
if (string.IsNullOrWhiteSpace(targetPath))
{
return false;
}
var fullTargetPath = Path.GetFullPath(targetPath);
if (!TryDeletePluginTarget(fullTargetPath))
{
RegisterPendingPluginDeletion(fullTargetPath);
}
RemovePluginFromSnapshot(pluginId);
RemovePluginFromCatalog(pluginId);
PendingRestartStateService.SetPending(PendingRestartStateService.PluginCatalogReason, true);
return true;
}
internal IReadOnlyList<InstalledPluginInfo> GetInstalledPluginsSnapshot()
@@ -219,16 +268,22 @@ public sealed class PluginRuntimeService : IDisposable
Directory.CreateDirectory(PluginsDirectory);
var manifest = ReadManifestFromPackage(fullPackagePath);
AppLogger.Info(
"PluginRuntime",
$"Installing package. PluginId='{manifest.Id}'; Source='{fullPackagePath}'; PluginsDirectory='{PluginsDirectory}'.");
var replacedExisting = RemoveExistingPluginPackages(manifest.Id, fullPackagePath);
var destinationPath = Path.Combine(PluginsDirectory, BuildInstalledPackageFileName(manifest.Id));
if (!string.Equals(fullPackagePath, Path.GetFullPath(destinationPath), StringComparison.OrdinalIgnoreCase))
{
File.Copy(fullPackagePath, destinationPath, overwrite: true);
FileOperationRetryHelper.CopyWithRetry(fullPackagePath, destinationPath, overwrite: true, "PluginRuntime");
}
UpdateCatalogAfterPackageInstall(manifest, destinationPath);
PendingRestartStateService.SetPending(PendingRestartStateService.PluginCatalogReason, true);
AppLogger.Info(
"PluginRuntime",
$"Package staged. PluginId='{manifest.Id}'; Destination='{destinationPath}'; ReplacedExisting={replacedExisting}.");
return new PluginPackageInstallResult(manifest, replacedExisting, RestartRequired: true);
}
@@ -349,7 +404,7 @@ public sealed class PluginRuntimeService : IDisposable
continue;
}
File.Delete(existingPackagePath);
FileOperationRetryHelper.DeleteFileWithRetry(existingPackagePath, "PluginRuntime");
replacedExisting = true;
}
catch
@@ -445,6 +500,150 @@ public sealed class PluginRuntimeService : IDisposable
}
}
private void ApplyPendingPluginDeletions()
{
var pendingPaths = ReadPendingPluginDeletions();
if (pendingPaths.Count == 0)
{
return;
}
var remainingPaths = new List<string>();
foreach (var path in pendingPaths)
{
if (!TryDeletePluginTarget(path))
{
remainingPaths.Add(path);
}
}
SavePendingPluginDeletions(remainingPaths);
}
private string ResolvePluginRemovalTargetPath(PluginCatalogEntry entry)
{
if (entry.IsPackage)
{
return entry.SourcePath;
}
var fullSourcePath = Path.GetFullPath(entry.SourcePath);
if (File.Exists(fullSourcePath) &&
string.Equals(Path.GetFileName(fullSourcePath), "plugin.json", StringComparison.OrdinalIgnoreCase))
{
return Path.GetDirectoryName(fullSourcePath) ?? fullSourcePath;
}
return fullSourcePath;
}
private static bool TryDeletePluginTarget(string targetPath)
{
try
{
if (File.Exists(targetPath))
{
File.Delete(targetPath);
}
else if (Directory.Exists(targetPath))
{
Directory.Delete(targetPath, recursive: true);
}
return !File.Exists(targetPath) && !Directory.Exists(targetPath);
}
catch
{
return false;
}
}
private void RegisterPendingPluginDeletion(string targetPath)
{
var pendingPaths = ReadPendingPluginDeletions();
if (pendingPaths.Contains(targetPath, StringComparer.OrdinalIgnoreCase))
{
return;
}
pendingPaths.Add(targetPath);
SavePendingPluginDeletions(pendingPaths);
}
private List<string> ReadPendingPluginDeletions()
{
var pendingDeletionFilePath = GetPendingDeletionFilePath();
if (!File.Exists(pendingDeletionFilePath))
{
return [];
}
try
{
var json = File.ReadAllText(pendingDeletionFilePath);
var paths = JsonSerializer.Deserialize<List<string>>(json);
return paths?
.Where(path => !string.IsNullOrWhiteSpace(path))
.Select(Path.GetFullPath)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList() ?? [];
}
catch
{
return [];
}
}
private void SavePendingPluginDeletions(IEnumerable<string> pendingPaths)
{
var pendingDeletionFilePath = GetPendingDeletionFilePath();
var normalizedPaths = pendingPaths
.Where(path => !string.IsNullOrWhiteSpace(path))
.Select(Path.GetFullPath)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (normalizedPaths.Length == 0)
{
if (File.Exists(pendingDeletionFilePath))
{
File.Delete(pendingDeletionFilePath);
}
return;
}
Directory.CreateDirectory(PluginsDirectory);
var json = JsonSerializer.Serialize(normalizedPaths, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(pendingDeletionFilePath, json);
}
private string GetPendingDeletionFilePath()
{
return Path.Combine(PluginsDirectory, PendingDeletionFileName);
}
private void RemovePluginFromSnapshot(string pluginId)
{
var snapshot = _appSettingsService.Load();
if (snapshot.DisabledPluginIds.RemoveAll(id => string.Equals(id, pluginId, StringComparison.OrdinalIgnoreCase)) > 0)
{
_appSettingsService.Save(snapshot);
}
}
private void RemovePluginFromCatalog(string pluginId)
{
_catalog.RemoveAll(entry => string.Equals(entry.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_settingsPages.RemoveAll(entry => string.Equals(entry.Plugin.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_desktopComponents.RemoveAll(entry => string.Equals(entry.Plugin.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
_loadResults.RemoveAll(entry => string.Equals(entry.Manifest?.Id, pluginId, StringComparison.OrdinalIgnoreCase));
}
private sealed record PluginCandidate(
string SourcePath,
PluginManifest Manifest,

View File

@@ -8,6 +8,8 @@ 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;
@@ -18,6 +20,7 @@ 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();
@@ -38,8 +41,9 @@ public partial class PluginSettingsPage : UserControl
{
PluginSystemStatusTextBlock.Text = L("settings.plugins.runtime_unavailable", "Plugin runtime is not available.");
PluginRuntimeSummaryPanel.Children.Clear();
PluginCatalogItemsHost.Children.Clear();
InstalledPluginsSettingsExpander.Items.Clear();
PluginRestartHintTextBlock.IsVisible = false;
PluginCatalogEmptyTextBlock.IsVisible = false;
return;
}
@@ -74,7 +78,7 @@ public partial class PluginSettingsPage : UserControl
"Detected {0} plugin(s); enabled {1}; loaded {2}; settings pages {3}; widgets {4}; failures {5}.",
runtime.Catalog.Count,
enabledCount,
runtime.LoadedPlugins.Count,
runtime.Catalog.Count(entry => entry.IsLoaded),
runtime.SettingsPages.Count,
runtime.DesktopComponents.Count,
failures.Length);
@@ -99,7 +103,7 @@ public partial class PluginSettingsPage : UserControl
private void BuildPluginCatalog(PluginRuntimeService runtime)
{
PluginCatalogItemsHost.Children.Clear();
InstalledPluginsSettingsExpander.Items.Clear();
var plugins = runtime.Catalog
.OrderBy(entry => entry.Manifest.Name, StringComparer.OrdinalIgnoreCase)
@@ -110,86 +114,20 @@ public partial class PluginSettingsPage : UserControl
foreach (var plugin in plugins)
{
PluginCatalogItemsHost.Children.Add(CreatePluginCatalogItem(runtime, plugin));
InstalledPluginsSettingsExpander.Items.Add(CreatePluginCatalogItem(runtime, plugin));
}
}
private Control CreatePluginCatalogItem(PluginRuntimeService runtime, PluginCatalogEntry entry)
private SettingsExpanderItem CreatePluginCatalogItem(PluginRuntimeService runtime, PluginCatalogEntry entry)
{
var title = new TextBlock
return new SettingsExpanderItem
{
Text = entry.Manifest.Name,
FontSize = 16,
FontWeight = FontWeight.SemiBold,
TextWrapping = TextWrapping.Wrap
Content = entry.Manifest.Name,
Description = BuildPluginSubtitle(entry),
IconSource = CreatePluginCatalogIconSource(),
IsClickEnabled = false,
Footer = CreatePluginCatalogActions(runtime, entry)
};
var subtitle = new TextBlock
{
Text = BuildPluginSubtitle(entry),
Foreground = PluginSystemDescriptionTextBlock.Foreground,
TextWrapping = TextWrapping.Wrap
};
var enabledToggle = new ToggleSwitch
{
IsChecked = entry.IsEnabled,
OnContent = L("settings.plugins.toggle_on", "Enabled"),
OffContent = L("settings.plugins.toggle_off", "Disabled"),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center
};
enabledToggle.IsCheckedChanged += (_, _) => OnPluginEnableChanged(runtime, entry, enabledToggle.IsChecked == true);
var header = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*,Auto"),
ColumnSpacing = 12,
Children =
{
new StackPanel
{
Spacing = 4,
Children = { title, subtitle }
},
enabledToggle
}
};
Grid.SetColumn(enabledToggle, 1);
var details = new TextBlock
{
Text = BuildPluginDetails(entry),
Foreground = PluginSystemDescriptionTextBlock.Foreground,
TextWrapping = TextWrapping.Wrap
};
return new Border
{
Background = new SolidColorBrush(Color.Parse("#14000000")),
CornerRadius = new CornerRadius(16),
Padding = new Thickness(14),
Child = new StackPanel
{
Spacing = 10,
Children = { header, details }
}
};
}
private void OnPluginEnableChanged(PluginRuntimeService runtime, PluginCatalogEntry entry, bool isEnabled)
{
runtime.SetPluginEnabled(entry.Manifest.Id, isEnabled);
BuildRuntimeSummary(runtime);
BuildPluginCatalog(runtime);
PluginSystemStatusTextBlock.Text = 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,
isEnabled
? L("settings.plugins.toggle_state_enabled", "enabled")
: L("settings.plugins.toggle_state_disabled", "disabled"));
}
private async void OnInstallPluginPackageClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -247,6 +185,7 @@ public partial class PluginSettingsPage : UserControl
var manifest = runtime.InstallPluginPackage(temporaryPackagePath);
RefreshFromRuntime();
RefreshPluginNavigation(TopLevel.GetTopLevel(this));
SetPackageImportStatus(
F(
"settings.plugins.install_success_format",
@@ -279,6 +218,79 @@ public partial class PluginSettingsPage : UserControl
}
}
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)
@@ -301,32 +313,13 @@ public partial class PluginSettingsPage : UserControl
private string BuildPluginSubtitle(PluginCatalogEntry entry)
{
var source = entry.IsPackage
? L("settings.plugins.source_package", ".laapp package")
: L("settings.plugins.source_manifest", "Loose manifest");
var state = entry.IsEnabled
? entry.IsLoaded
? L("settings.plugins.state.loaded", "Loaded")
: L("settings.plugins.state.load_failed", "Load failed")
: L("settings.plugins.state.disabled", "Disabled");
var publisher = string.IsNullOrWhiteSpace(entry.Manifest.Author)
? L("settings.plugins.publisher_unknown", "Unknown publisher")
: entry.Manifest.Author;
return F(
"settings.plugins.subtitle_format",
"{0} | {1} | {2}",
state,
source,
entry.Manifest.Id);
}
private string BuildPluginDetails(PluginCatalogEntry entry)
{
var detail = F(
"settings.plugins.detail_format",
"Settings pages: {0} | Widgets: {1}",
entry.SettingsPageCount,
entry.WidgetCount);
return string.IsNullOrWhiteSpace(entry.ErrorMessage)
? detail
: detail + Environment.NewLine + entry.ErrorMessage;
"settings.plugins.publisher_format",
"Publisher: {0}",
publisher);
}
private TextBlock CreateSummaryLine(string text)
@@ -350,6 +343,75 @@ public partial class PluginSettingsPage : UserControl
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

View File

@@ -44,31 +44,13 @@
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="InstalledPluginsSettingsExpander"
Header="Installed Plugins"
Description="Enable plugins here. Detailed settings appear as separate settings pages."
Description="Manage installed plugins here."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource Glyph="&#xe8fd;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Padding="14">
<StackPanel Spacing="10">
<Button x:Name="InstallPluginPackageButton"
HorizontalAlignment="Left"
Click="OnInstallPluginPackageClick"
Content="Open .laapp package" />
<TextBlock x:Name="PluginPackageImportHintTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
TextWrapping="Wrap"
Text="Open a .laapp package to install it into the local plugin directory." />
<TextBlock x:Name="PluginPackageImportStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
TextWrapping="Wrap"
IsVisible="False" />
</StackPanel>
</Border>
<TextBlock x:Name="PluginRestartHintTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
TextWrapping="Wrap"
@@ -77,7 +59,33 @@
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="No plugins found."
IsVisible="False" />
<StackPanel x:Name="PluginCatalogItemsHost" Spacing="12" />
</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>
<ui:FontIconSource Glyph="&#xe8b7;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
</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 AdaptiveTextSecondaryBrush}"
TextWrapping="Wrap"
Text="Open a .laapp package to install it into the local plugin directory." />
<TextBlock x:Name="PluginPackageImportStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
TextWrapping="Wrap"
IsVisible="False" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>

View File

@@ -9,6 +9,7 @@ public partial class SettingsWindow
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

@@ -10,8 +10,10 @@ public partial class SettingsWindow
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", "Enable or disable plugins here. Detailed plugin settings appear as separate settings pages.");
PluginRestartHintTextBlock.Text = L("settings.plugins.restart_hint", "Plugin enable state changes take effect after restarting the app.");
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();
}