mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-18 00:18:00 +00:00
Added export and import of plugin archives (#885)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
e7407ce60a
commit
8a9e6aeb87
43 files changed
+2041
-256
No files matched your search
@@ -312,7 +312,8 @@
|
||||
<ThirdPartyComponent Name="base64" Developer="Marshall Pierce, Alice Maz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/marshallpierce/rust-base64/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/marshallpierce/rust-base64" UseCase="@T("For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.")"/>
|
||||
<ThirdPartyComponent Name="Rust Crypto" Developer="Artyom Pavlov, Tony Arcieri, Brian Warner, Arthur Gautier, Vlad Filippov, Friedel Ziegelmayer, Nicolas Stalder & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/RustCrypto/traits/blob/master/cipher/LICENSE-MIT" RepositoryUrl="https://github.com/RustCrypto" UseCase="@T("When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project.")"/>
|
||||
<ThirdPartyComponent Name="rcgen" Developer="RustTLS developers, est31 & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rcgen/blob/main/LICENSE" RepositoryUrl="https://github.com/rustls/rcgen" UseCase="@T("For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose.")"/>
|
||||
<ThirdPartyComponent Name="windows-registry" Developer="Microsoft, Kenny Kerr, Ryan Levick, Rafael Rivera, sivadeilra, Marijn Suijten & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/microsoft/windows-rs/blob/master/license-mit" RepositoryUrl="https://github.com/microsoft/windows-rs" UseCase="@T("This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration.")"/>
|
||||
<ThirdPartyComponent Name="windows-rs" Developer="Microsoft, Kenny Kerr, Ryan Levick, Rafael Rivera, sivadeilra, Marijn Suijten & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/microsoft/windows-rs/blob/master/license-mit" RepositoryUrl="https://github.com/microsoft/windows-rs" UseCase="@T("The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.")"/>
|
||||
<ThirdPartyComponent Name="objc2" Developer="Steven Sheldon, Mads Marquart, silvanshade, Dzmitry Malyshau, Felix Nemo Kaaman, adamnemecek, Samuel Sleight, Paul Mabileau & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/madsmtm/objc2/blob/main/LICENSE-MIT.txt" RepositoryUrl="https://github.com/madsmtm/objc2" UseCase="@T("The objc2 project provides access to Apple's Objective-C frameworks from Rust. On macOS, we use the libraries objc2, objc2-app-kit, and objc2-foundation to open the native macOS share sheet, e.g., when you share a plugin with others.")"/>
|
||||
<ThirdPartyComponent Name="file-format" Developer="Mickaël Malécot & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mmalecot/file-format/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/mmalecot/file-format" UseCase="@T("This library identifies files by their content. It is used for document streaming and as the first safety and media classification step before local audio processing.")"/>
|
||||
<ThirdPartyComponent Name="Symphonia" Developer="Philip Deljanov & Open Source Community" LicenseName="MPL-2.0" LicenseUrl="https://github.com/pdeljanov/Symphonia/blob/v0.6.0/LICENSE" RepositoryUrl="https://github.com/pdeljanov/Symphonia" UseCase="@T("Symphonia is used for media container demuxing and audio decoding. The exact MPL-covered source is available from the repository linked and is identified in the offline notices bundled with AI Studio.")"/>
|
||||
<ThirdPartyComponent Name="Ropus" Developer="0x4D44, Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo, Mozilla, Amazon & Open Source Community" LicenseName="BSD-3-Clause" LicenseUrl="https://github.com/0x4D44/ropus/blob/main/LICENSE" RepositoryUrl="https://github.com/0x4d44/ropus" UseCase="@T("Ropus provides the Opus encoder and decoder used by the media pipeline.")"/>
|
||||
|
||||
@@ -5,13 +5,26 @@
|
||||
@attribute [Route(Routes.PLUGINS)]
|
||||
|
||||
<div class="inner-scrolling-context">
|
||||
<MudText Typo="Typo.h3" Class="mb-2">
|
||||
@T("Plugins")
|
||||
</MudText>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-2">
|
||||
<MudText Typo="Typo.h3">
|
||||
@T("Plugins")
|
||||
</MudText>
|
||||
<MudSpacer />
|
||||
<LockableButton Text="@T("Import")"
|
||||
Tooltip="@T("Import assistant plugin")"
|
||||
Icon="@IMPORT_ICON"
|
||||
ButtonVariant="Variant.Outlined"
|
||||
ButtonColor="Color.Default"
|
||||
NoMargin="@true"
|
||||
Class="flex-none"
|
||||
Disabled="@(() => this.isImportingAssistantPlugin)"
|
||||
IsLocked="@(() => !this.AllowPluginImport)"
|
||||
OnClickAsync="@this.ImportAssistantPluginAsync"/>
|
||||
</MudStack>
|
||||
|
||||
<InnerScrolling>
|
||||
|
||||
<MudTable Items="@PluginFactory.AvailablePlugins" Hover="@true" GroupBy="@this.groupConfig" Class="border-dashed border rounded-lg">
|
||||
<MudTable Items="@PluginFactory.AvailablePlugins" Hover="@true" GroupBy="@this.groupConfig" Class="@this.PluginTableClass">
|
||||
<ColGroup>
|
||||
<col style="width: 2em;" />
|
||||
<col style="width: 2.1em; "/>
|
||||
@@ -65,12 +78,13 @@
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudStack Row="true" Spacing="0" AlignItems="AlignItems.Center">
|
||||
<MudStack Row="true" Spacing="0" AlignItems="AlignItems.Center" Justify="Justify.FlexEnd">
|
||||
@if (context.Type is PluginType.ASSISTANT)
|
||||
{
|
||||
var assistantPlugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == context.Id);
|
||||
<AssistantPluginSecurityCard Plugin="@assistantPlugin" Compact="@true"/>
|
||||
}
|
||||
|
||||
@if (context is { IsInternal: false, Type: not PluginType.CONFIGURATION })
|
||||
{
|
||||
var isEnabled = this.SettingsManager.IsPluginEnabled(context);
|
||||
@@ -80,7 +94,7 @@
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<MudButtonGroup Class="ms-3">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0" Class="ms-3" Style="gap: 4px;">
|
||||
@if (context is { IsInternal: false } && !string.IsNullOrWhiteSpace(context.SourceURL))
|
||||
{
|
||||
var sourceUrl = context.SourceURL;
|
||||
@@ -89,7 +103,7 @@
|
||||
{
|
||||
var isDefaultSupportContact = string.Equals(sourceUrl, AssistantPluginGenerationService.DEFAULT_SUPPORT_CONTACT, StringComparison.Ordinal);
|
||||
<MudTooltip Text="@(isDefaultSupportContact ? string.Empty : T("Send a mail"))">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Email" Href="@sourceUrl" Target="_blank" Size="Size.Medium" Disabled="@isDefaultSupportContact"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Email" Href="@sourceUrl" Target="_blank" Size="Size.Medium" Disabled="@isDefaultSupportContact"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
@@ -107,6 +121,13 @@
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Code" Size="Size.Medium" OnClick="@(() => this.OpenAssistantPluginEditorDialogAsync(editablePlugin))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (context is IAvailablePlugin shareablePlugin && CanSharePlugin(shareablePlugin))
|
||||
{
|
||||
<MudTooltip Text="@(this.AllowPluginSharing ? this.SharePluginTooltip : this.SharePluginLockText)">
|
||||
<MudIconButton Icon="@SharePluginIcon" Size="Size.Medium" OnClick="@(() => this.SharePluginAsync(shareablePlugin))" Disabled="@(this.isSharingPlugin || !this.AllowPluginSharing)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (context is IAvailablePlugin revisionPlugin && CanReviseAssistantPlugin(revisionPlugin))
|
||||
{
|
||||
@@ -119,7 +140,7 @@
|
||||
{
|
||||
<AssistantPluginDeleteAction Plugin="@availablePlugin" />
|
||||
}
|
||||
</MudButtonGroup>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
|
||||
@@ -4,7 +4,8 @@ using AIStudio.Dialogs;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
@@ -16,6 +17,7 @@ public partial class Plugins : MSGComponentBase
|
||||
private const string GROUP_DISABLED = "Disabled";
|
||||
private const string GROUP_INTERNAL = "Internal";
|
||||
private bool isAutoAuditing;
|
||||
private bool isImportingAssistantPlugin;
|
||||
|
||||
private DataAssistantPluginAudit AssistantPluginAuditSettings => this.SettingsManager.ConfigurationData.AssistantPluginAudit;
|
||||
|
||||
@@ -27,14 +29,43 @@ public partial class Plugins : MSGComponentBase
|
||||
[Inject]
|
||||
private AssistantPluginAuditService AssistantPluginAuditService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private PluginShareService PluginShareService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AssistantPluginInstallService AssistantPluginInstallService { get; init; } = null!;
|
||||
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(Plugins));
|
||||
|
||||
private bool isSharingPlugin;
|
||||
|
||||
/// <summary>
|
||||
/// Number of active drop areas above this page. While there is any, another component owns the
|
||||
/// dropped files and this page must not catch them.
|
||||
/// </summary>
|
||||
private uint numDropAreasAboveThis;
|
||||
|
||||
private bool isDraggingOverPage;
|
||||
|
||||
private const string IMPORT_ICON =
|
||||
@"<svg class=""mud-icon-root mud-svg-icon mud-dark-text mud-icon-size-medium"" focusable=""false"" viewBox=""0 0 24 24"" aria-hidden=""true"" role=""img"">
|
||||
<path d=""M0 0h24v24H0V0z"" fill=""none""></path>
|
||||
<path d=""M16 5l-1.42 1.42-1.59-1.59V16h-1.98V4.83L9.42 6.42 8 5l4-4 4 4z"" transform=""rotate(180 12 10)""></path>
|
||||
<path d=""M20 10v11c0 1.1-.9 2-2 2H6c-1.11 0-2-.9-2-2V10c0-1.11.89-2 2-2h3v2H6v11h12V10h-3V8h3c1.1 0 2 .89 2 2z""></path>
|
||||
</svg>";
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.PLUGINS_RELOADED ]);
|
||||
|
||||
this.ApplyFilters([], [ Event.PLUGINS_RELOADED, Event.CONFIGURATION_CHANGED, Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
|
||||
|
||||
// Register the whole page as a drop area, so users can drop a plugin archive anywhere on it:
|
||||
await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, DropLayers.PAGES);
|
||||
|
||||
this.groupConfig = new TableGroupDefinition<IPluginMetadata>
|
||||
{
|
||||
Expandable = true,
|
||||
@@ -59,6 +90,13 @@ public partial class Plugins : MSGComponentBase
|
||||
await this.TryAutoAuditAssistantsAsync();
|
||||
}
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
// Release the drop area again, so lower layers can catch dropped files:
|
||||
_ = this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, DropLayers.PAGES);
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task PluginActivationStateChanged(IPluginMetadata pluginMeta)
|
||||
@@ -184,16 +222,54 @@ public partial class Plugins : MSGComponentBase
|
||||
: this.T("Enable plugin");
|
||||
}
|
||||
|
||||
//
|
||||
// These methods decide whether an action exists for a plugin at all. They must not depend on
|
||||
// transient state like an ongoing share: they gate the markup, so a transient value would make
|
||||
// the action buttons disappear and reappear. Transient state belongs into the buttons' Disabled.
|
||||
//
|
||||
private static bool CanEditAssistantPlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, Type: PluginType.ASSISTANT } && !string.IsNullOrWhiteSpace(plugin.LocalPath);
|
||||
|
||||
private static bool CanReviseAssistantPlugin(IAvailablePlugin plugin)
|
||||
{
|
||||
var assistantPlugin = PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == plugin.Id);
|
||||
return plugin is { IsInternal: false, IsManagedByConfigServer: false, Type: PluginType.ASSISTANT } &&
|
||||
!string.IsNullOrWhiteSpace(plugin.LocalPath) &&
|
||||
assistantPlugin?.IsManagedByConfigServer is false;
|
||||
return plugin is { IsInternal: false, IsManagedByConfigServer: false, Type: PluginType.ASSISTANT } && !string.IsNullOrWhiteSpace(plugin.LocalPath) && assistantPlugin?.IsManagedByConfigServer is false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sharing is limited to assistant plugins because the import accepts only those. Otherwise,
|
||||
/// users would create archives nobody can install. Widen this once the import supports more
|
||||
/// plugin types.
|
||||
/// </summary>
|
||||
private static bool CanSharePlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, IsManagedByConfigServer: false, Type: PluginType.ASSISTANT } && !string.IsNullOrWhiteSpace(plugin.LocalPath);
|
||||
|
||||
/// <summary>
|
||||
/// Highlights the plugin table while the user drags a file over the page, so it is visible
|
||||
/// where the file would land.
|
||||
/// </summary>
|
||||
private string PluginTableClass => this.isDraggingOverPage
|
||||
? "border-dashed border rounded-lg mud-border-primary border-4"
|
||||
: "border-dashed border rounded-lg";
|
||||
|
||||
/// <summary>
|
||||
/// Organizations may disable importing plugin archives by using a configuration plugin.
|
||||
/// </summary>
|
||||
private bool AllowPluginImport => this.SettingsManager.ConfigurationData.App.AllowUserToImportPlugins;
|
||||
|
||||
/// <summary>
|
||||
/// Organizations may disable sharing and exporting plugins by using a configuration plugin.
|
||||
/// </summary>
|
||||
private bool AllowPluginSharing => this.SettingsManager.ConfigurationData.App.AllowUserToSharePlugins;
|
||||
|
||||
/// <summary>
|
||||
/// Linux has no native share sheet, hence the plugin archive is exported to a location of the
|
||||
/// user's choice there. The action must be labeled accordingly.
|
||||
/// </summary>
|
||||
private static string SharePluginIcon => OperatingSystem.IsLinux() ? Icons.Material.Filled.FileDownload : Icons.Material.Filled.IosShare;
|
||||
|
||||
private string SharePluginTooltip => OperatingSystem.IsLinux() ? this.T("Export plugin archive") : this.T("Share plugin archive");
|
||||
|
||||
private string SharePluginLockText => OperatingSystem.IsLinux() ? this.T("Your organization has disabled exporting plugins.") : this.T("Your organization has disabled sharing plugins.");
|
||||
|
||||
private async Task OpenAssistantPluginEditorDialogAsync(IAvailablePlugin plugin)
|
||||
{
|
||||
var parameters = new DialogParameters<AssistantPluginEditorDialog>
|
||||
@@ -209,7 +285,9 @@ public partial class Plugins : MSGComponentBase
|
||||
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Save, string.Format(this.T("The assistant plugin '{0}' has been successfully saved."), result.PluginName)));
|
||||
LOG.LogInformation($"The assistant plugin '{result.PluginName}' ({result.PluginId}) has been successfully updated.");
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.PLUGINS_RELOADED);
|
||||
|
||||
// Saving the plugin ran LoadAll, which already sent PLUGINS_RELOADED. Editing the plugin
|
||||
// code changes no settings, so there is nothing else to announce:
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
|
||||
@@ -228,11 +306,139 @@ public partial class Plugins : MSGComponentBase
|
||||
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.AutoFixHigh, string.Format(this.T("The assistant plugin '{0}' has been successfully revised."), result.PluginName)));
|
||||
LOG.LogInformation($"The assistant plugin '{result.PluginName}' ({result.PluginId}) has been successfully revised.");
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.PLUGINS_RELOADED);
|
||||
|
||||
// Saving the revision ran LoadAll, which already sent PLUGINS_RELOADED. We still announce the
|
||||
// configuration change: with automatic audits enabled, the dialog stored an audit result:
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task SharePluginAsync(IAvailablePlugin plugin)
|
||||
{
|
||||
if (this.isSharingPlugin)
|
||||
return;
|
||||
|
||||
this.isSharingPlugin = true;
|
||||
// invoke a state change right away to guard action buttons
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
var shareResult = await this.PluginShareService.ShareAsync(plugin, CancellationToken.None);
|
||||
if (shareResult.Cancelled)
|
||||
return;
|
||||
|
||||
if (!shareResult.Success)
|
||||
{
|
||||
LOG.LogError($"Sharing the plugin '{shareResult.PluginName}' from archive '{shareResult.ArchivePath}' failed with Issue: '{shareResult.Issue}'.");
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.ReportProblem, OperatingSystem.IsLinux() ? T("An error occurred while exporting the plugin.") : T("An error occurred while sharing the plugin.")));
|
||||
return;
|
||||
}
|
||||
|
||||
// On Linux, the user chose the target location, so we confirm where the archive was stored:
|
||||
if (OperatingSystem.IsLinux())
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.FileDownload, string.Format(T("The plugin archive was exported to '{0}'."), shareResult.ArchivePath)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isSharingPlugin = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ImportAssistantPluginAsync()
|
||||
{
|
||||
if (this.isImportingAssistantPlugin)
|
||||
return;
|
||||
|
||||
if (!this.AllowPluginImport)
|
||||
return;
|
||||
|
||||
var selection = await this.RustService.SelectFile(this.T("Import assistant plugin"), [FileTypes.PLUGIN_ARCHIVE]);
|
||||
if (selection.UserCancelled)
|
||||
return;
|
||||
|
||||
await this.ImportPluginArchiveAsync(selection.SelectedFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a plugin archive, no matter whether the user picked it through the import button or
|
||||
/// dropped it onto the page.
|
||||
/// </summary>
|
||||
/// <param name="archivePath">The local plugin archive to install.</param>
|
||||
private async Task ImportPluginArchiveAsync(string archivePath)
|
||||
{
|
||||
if (this.isImportingAssistantPlugin)
|
||||
return;
|
||||
|
||||
if (!this.AllowPluginImport)
|
||||
return;
|
||||
|
||||
this.isImportingAssistantPlugin = true;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await this.AssistantPluginInstallService.InstallArchiveAsync(archivePath, this.ConfirmPluginImportAsync, CancellationToken.None);
|
||||
if (result.Cancelled)
|
||||
return;
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
LOG.LogError("Failed to import assistant plugin archive '{ArchivePath}': {Issue}", archivePath, result.Issue);
|
||||
|
||||
// The user actively started this import, so we report the reason in a dialog
|
||||
// instead of a snackbar. Refused imports must not be missed:
|
||||
await this.ShowImportRefusedDialogAsync(result.Issue);
|
||||
return;
|
||||
}
|
||||
|
||||
var message = result.ReplacedExisting
|
||||
? this.T("Assistant updated.")
|
||||
: this.T("Assistant installed.");
|
||||
|
||||
// We do not announce the reload ourselves: a successful installation ran LoadAll, which
|
||||
// already sent PLUGINS_RELOADED. The import changes no settings either, so there is
|
||||
// nothing to report as a configuration change:
|
||||
await this.MessageBus.SendSuccess(new(Icons.Material.Filled.Extension, message));
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isImportingAssistantPlugin = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the metadata of a validated plugin archive and asks whether it may be installed.
|
||||
/// </summary>
|
||||
/// <param name="preview">The metadata the archive declares about itself.</param>
|
||||
/// <returns>True when the user confirmed the installation.</returns>
|
||||
private async Task<bool> ConfirmPluginImportAsync(PluginImportPreview preview)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<PluginImportDialog>
|
||||
{
|
||||
{ x => x.Preview, preview },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<PluginImportDialog>(this.T("Install Plugin"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
return dialogResult is { Canceled: false };
|
||||
}
|
||||
|
||||
private async Task ShowImportRefusedDialogAsync(string issue)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<InformationDialog>
|
||||
{
|
||||
{ x => x.Message, string.Format(this.T("The plugin could not be imported: {0}"), issue) },
|
||||
{ x => x.Icon, Icons.Material.Filled.ReportProblem },
|
||||
{ x => x.IconColor, Color.Error },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<InformationDialog>(this.T("Import not possible"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
await dialogReference.Result;
|
||||
}
|
||||
|
||||
private static bool IsSendingMail(string sourceUrl) => sourceUrl.TrimStart().StartsWith("mailto:", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private PluginAssistants? TryGetAssistantPlugin(Guid pluginId) => PluginFactory.RunningPlugins.OfType<PluginAssistants>().FirstOrDefault(x => x.Id == pluginId);
|
||||
@@ -302,8 +508,71 @@ public partial class Plugins : MSGComponentBase
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
if (data is int registeredLayer && registeredLayer > DropLayers.PAGES)
|
||||
this.numDropAreasAboveThis++;
|
||||
|
||||
break;
|
||||
|
||||
case Event.UNREGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
if (data is int unregisteredLayer && unregisteredLayer > DropLayers.PAGES && this.numDropAreasAboveThis > 0)
|
||||
this.numDropAreasAboveThis--;
|
||||
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
|
||||
if (!this.CanCatchDroppedFile())
|
||||
return;
|
||||
|
||||
this.isDraggingOverPage = true;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
|
||||
this.isDraggingOverPage = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var droppedPaths }:
|
||||
this.isDraggingOverPage = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
if (!this.CanCatchDroppedFile())
|
||||
return;
|
||||
|
||||
await this.ImportDroppedPluginArchiveAsync(droppedPaths);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decides whether this page may process dropped files: only when no drop area above it is
|
||||
/// active and when the organization allows importing plugins at all.
|
||||
/// </summary>
|
||||
private bool CanCatchDroppedFile() => this.numDropAreasAboveThis is 0 && this.AllowPluginImport && !this.isImportingAssistantPlugin;
|
||||
|
||||
/// <summary>
|
||||
/// Imports a plugin archive the user dropped onto the page. Anything that is not exactly one
|
||||
/// plugin archive is reported instead of guessing what the user meant.
|
||||
/// </summary>
|
||||
/// <param name="droppedPaths">The paths of the dropped files.</param>
|
||||
private async Task ImportDroppedPluginArchiveAsync(IReadOnlyList<string> droppedPaths)
|
||||
{
|
||||
var archivePaths = droppedPaths.Where(path => FileTypes.IsAllowedPath(path, FileTypes.PLUGIN_ARCHIVE)).ToList();
|
||||
switch (archivePaths.Count)
|
||||
{
|
||||
case 0:
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.ReportProblem, string.Format(this.T("Please drop a plugin archive with the extension {0} or .zip."), PluginArchive.PLUGIN_FILE_EXTENSION)));
|
||||
return;
|
||||
|
||||
case > 1:
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.ReportProblem, this.T("Please drop only one plugin archive at a time.")));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.ImportPluginArchiveAsync(archivePaths[0]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user