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