2025-04-12 19:13:33 +00:00
using AIStudio.Components ;
2026-04-09 08:01:24 +00:00
using AIStudio.Agents.AssistantAudit ;
using AIStudio.Dialogs ;
using AIStudio.Settings.DataModel ;
using AIStudio.Tools.PluginSystem.Assistants ;
2025-03-29 17:40:17 +00:00
using AIStudio.Tools.PluginSystem ;
2026-08-06 08:42:20 +00:00
using AIStudio.Tools.Rust ;
using AIStudio.Tools.Services ;
2025-04-24 11:50:14 +00:00
using Microsoft.AspNetCore.Components ;
2026-04-09 08:01:24 +00:00
using DialogOptions = AIStudio . Dialogs . DialogOptions ;
2025-04-24 11:50:14 +00:00
2025-03-29 17:40:17 +00:00
namespace AIStudio.Pages ;
2025-04-12 19:13:33 +00:00
public partial class Plugins : MSGComponentBase
2025-03-29 17:40:17 +00:00
{
private const string GROUP_ENABLED = "Enabled" ;
private const string GROUP_DISABLED = "Disabled" ;
private const string GROUP_INTERNAL = "Internal" ;
2026-04-09 08:01:24 +00:00
private bool isAutoAuditing ;
2026-08-06 08:42:20 +00:00
private bool isImportingAssistantPlugin ;
2026-04-09 08:01:24 +00:00
private DataAssistantPluginAudit AssistantPluginAuditSettings = > this . SettingsManager . ConfigurationData . AssistantPluginAudit ;
2025-03-29 17:40:17 +00:00
private TableGroupDefinition < IPluginMetadata > groupConfig = null ! ;
2026-04-09 08:01:24 +00:00
[Inject]
private IDialogService DialogService { get ; init ; } = null ! ;
[Inject]
private AssistantPluginAuditService AssistantPluginAuditService { get ; init ; } = null ! ;
2026-08-06 08:42:20 +00:00
[Inject]
private PluginShareService PluginShareService { get ; init ; } = null ! ;
[Inject]
private RustService RustService { get ; init ; } = null ! ;
[Inject]
2026-08-09 17:01:58 +00:00
private PluginInstallService PluginInstallService { get ; init ; } = null ! ;
2026-08-06 08:42:20 +00:00
2026-07-15 19:00:25 +00:00
private static readonly ILogger LOG = Program . LOGGER_FACTORY . CreateLogger ( nameof ( Plugins ) ) ;
2026-08-06 08:42:20 +00:00
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 0 h24v24H0V0z "" fill = "" none "" > < / path >
< path d = "" M16 5l - 1.42 1.42 - 1.59 - 1.59 V16h - 1.98 V4 . 83L 9.42 6.42 8 5l 4 - 4 4 4 z "" transform = "" rotate ( 180 12 10 ) "" > < / path >
< path d = "" M20 10 v11c0 1.1 - . 9 2 - 2 2 H6c - 1.11 0 - 2 - . 9 - 2 - 2 V10c0 - 1.11 . 89 - 2 2 - 2 h3v2H6v11h12V10h - 3 V8h3c1 . 1 0 2 . 89 2 2 z "" > < / path >
< / svg > ";
2026-07-15 19:00:25 +00:00
2025-03-29 17:40:17 +00:00
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync ( )
{
2026-08-06 08:42:20 +00:00
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 ) ;
2025-03-29 17:40:17 +00:00
this . groupConfig = new TableGroupDefinition < IPluginMetadata >
{
Expandable = true ,
IsInitiallyExpanded = true ,
Selector = pluginMeta = >
{
if ( pluginMeta . IsInternal )
return GROUP_INTERNAL ;
return this . SettingsManager . IsPluginEnabled ( pluginMeta )
? GROUP_ENABLED
: GROUP_DISABLED ;
}
} ;
await base . OnInitializedAsync ( ) ;
}
2026-04-09 08:01:24 +00:00
protected override async Task OnAfterRenderAsync ( bool firstRender )
{
if ( firstRender )
await this . TryAutoAuditAssistantsAsync ( ) ;
}
2026-08-06 08:42:20 +00:00
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 ( ) ;
}
2025-03-29 17:40:17 +00:00
#endregion
2025-04-12 19:13:33 +00:00
2025-03-29 17:40:17 +00:00
private async Task PluginActivationStateChanged ( IPluginMetadata pluginMeta )
{
if ( this . SettingsManager . IsPluginEnabled ( pluginMeta ) )
2026-04-09 08:01:24 +00:00
{
2025-03-29 17:40:17 +00:00
this . SettingsManager . ConfigurationData . EnabledPlugins . Remove ( pluginMeta . Id ) ;
2026-04-09 08:01:24 +00:00
await this . SettingsManager . StoreSettings ( ) ;
await this . MessageBus . SendMessage < bool > ( this , Event . CONFIGURATION_CHANGED ) ;
return ;
}
if ( pluginMeta . Type is not PluginType . ASSISTANT )
{
2025-03-29 17:40:17 +00:00
this . SettingsManager . ConfigurationData . EnabledPlugins . Add ( pluginMeta . Id ) ;
2026-04-09 08:01:24 +00:00
await this . SettingsManager . StoreSettings ( ) ;
await this . MessageBus . SendMessage < bool > ( this , Event . CONFIGURATION_CHANGED ) ;
return ;
}
var assistantPlugin = PluginFactory . RunningPlugins . OfType < PluginAssistants > ( ) . FirstOrDefault ( x = > x . Id = = pluginMeta . Id ) ;
if ( assistantPlugin is null )
return ;
var securityState = PluginAssistantSecurityResolver . Resolve ( this . SettingsManager , assistantPlugin ) ;
if ( securityState . RequiresAudit )
{
await this . OpenAssistantAuditDialogAsync ( pluginMeta . Id ) ;
return ;
}
2026-07-15 19:00:25 +00:00
if ( securityState is { IsBelowMinimum : true , IsBlocked : true } )
2026-04-09 08:01:24 +00:00
{
var blockedAudit = securityState . Audit ;
if ( blockedAudit is not null )
await this . DialogService . ShowMessageBox ( this . T ( "Assistant Audit" ) , $"{blockedAudit.Level.GetName()}: {blockedAudit.Summary}" , this . T ( "Close" ) ) ;
return ;
}
2026-07-15 19:00:25 +00:00
if ( securityState is { IsBelowMinimum : true , CanOverride : true } & &
2026-04-09 08:01:24 +00:00
! await this . ConfirmActivationBelowMinimumAsync ( pluginMeta . Name , securityState . Audit ! . Level ) )
{
return ;
}
this . SettingsManager . ConfigurationData . EnabledPlugins . Add ( pluginMeta . Id ) ;
2025-03-29 17:40:17 +00:00
await this . SettingsManager . StoreSettings ( ) ;
await this . MessageBus . SendMessage < bool > ( this , Event . CONFIGURATION_CHANGED ) ;
}
2026-04-09 08:01:24 +00:00
private async Task OpenAssistantAuditDialogAsync ( Guid pluginId )
{
var parameters = new DialogParameters < AssistantPluginAuditDialog >
{
{ x = > x . PluginId , pluginId } ,
} ;
var dialog = await this . DialogService . ShowAsync < AssistantPluginAuditDialog > ( this . T ( "Assistant Audit" ) , parameters , DialogOptions . FULLSCREEN ) ;
var result = await dialog . Result ;
if ( result is null | | result . Canceled | | result . Data is not AssistantPluginAuditDialogResult auditResult )
return ;
if ( auditResult . Audit is not null )
this . UpsertAuditCard ( auditResult . Audit ) ;
if ( auditResult . ActivatePlugin )
this . SettingsManager . ConfigurationData . EnabledPlugins . Add ( pluginId ) ;
await this . SettingsManager . StoreSettings ( ) ;
await this . MessageBus . SendMessage < bool > ( this , Event . CONFIGURATION_CHANGED ) ;
}
private async Task < bool > ConfirmActivationBelowMinimumAsync ( string pluginName , AssistantAuditLevel actualLevel )
{
var dialogParameters = new DialogParameters < ConfirmDialog >
{
{
x = > x . Message ,
string . Format (
2026-08-25 14:53:19 +00:00
this . T ( "The assistant plugin '{0}' was audited with the level '{1}', which is below the required minimum level '{2}'. Your current settings allow activation anyway, but this may be potentially dangerous. Do you really want to enable this plugin?" ) ,
2026-04-09 08:01:24 +00:00
pluginName ,
actualLevel . GetName ( ) ,
this . AssistantPluginAuditSettings . MinimumLevel . GetName ( ) )
} ,
} ;
var dialogReference = await this . DialogService . ShowAsync < ConfirmDialog > ( this . T ( "Potentially Dangerous Plugin" ) , dialogParameters ,
DialogOptions . FULLSCREEN ) ;
var dialogResult = await dialogReference . Result ;
return dialogResult is not null & & ! dialogResult . Canceled ;
}
2026-02-10 14:23:56 +00:00
2026-04-09 08:01:24 +00:00
private bool IsActivationSwitchDisabled ( IPluginMetadata pluginMeta , bool isEnabled )
{
if ( isEnabled | | pluginMeta . Type is not PluginType . ASSISTANT )
return false ;
var assistantPlugin = this . TryGetAssistantPlugin ( pluginMeta . Id ) ;
if ( assistantPlugin is null )
return false ;
var securityState = PluginAssistantSecurityResolver . Resolve ( this . SettingsManager , assistantPlugin ) ;
2026-07-15 19:00:25 +00:00
return securityState is { IsBlocked : true , RequiresAudit : false } ;
2026-04-09 08:01:24 +00:00
}
private string GetActivationTooltip ( IPluginMetadata pluginMeta , bool isEnabled )
{
if ( isEnabled )
return this . T ( "Disable plugin" ) ;
if ( pluginMeta . Type is not PluginType . ASSISTANT )
return this . T ( "Enable plugin" ) ;
var assistantPlugin = this . TryGetAssistantPlugin ( pluginMeta . Id ) ;
if ( assistantPlugin is null )
return this . T ( "Enable plugin" ) ;
var securityState = PluginAssistantSecurityResolver . Resolve ( this . SettingsManager , assistantPlugin ) ;
if ( securityState . RequiresAudit )
return securityState . ActionLabel ;
return securityState . IsBlocked
? securityState . Description
: this . T ( "Enable plugin" ) ;
}
2026-08-06 08:42:20 +00:00
//
// 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.
//
2026-07-15 19:00:25 +00:00
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 ) ;
2026-08-06 08:42:20 +00:00
return plugin is { IsInternal : false , IsManagedByConfigServer : false , Type : PluginType . ASSISTANT } & & ! string . IsNullOrWhiteSpace ( plugin . LocalPath ) & & assistantPlugin ? . IsManagedByConfigServer is false ;
2026-07-15 19:00:25 +00:00
}
2026-08-06 08:42:20 +00:00
/// <summary>
2026-08-09 17:01:58 +00:00
/// The plugin types users may share. This list has to match what the import accepts, otherwise
/// users would create archives nobody can install.
2026-08-06 08:42:20 +00:00
/// </summary>
2026-08-09 17:01:58 +00:00
private static readonly PluginType [ ] SHAREABLE_PLUGIN_TYPES = [ PluginType . ASSISTANT , PluginType . CONFIGURATION , PluginType . LANGUAGE ] ;
/// <summary>
/// Checks whether a plugin may be shared or exported as an archive. Plugins shipped with
/// AI Studio and plugins deployed by an organization stay with their owner.
/// </summary>
private static bool CanSharePlugin ( IAvailablePlugin plugin ) = > plugin is { IsInternal : false , IsManagedByConfigServer : false } & & SHAREABLE_PLUGIN_TYPES . Contains ( plugin . Type ) & & ! string . IsNullOrWhiteSpace ( plugin . LocalPath ) ;
2026-08-06 08:42:20 +00:00
/// <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." ) ;
2026-07-15 19:00:25 +00:00
private async Task OpenAssistantPluginEditorDialogAsync ( IAvailablePlugin plugin )
{
var parameters = new DialogParameters < AssistantPluginEditorDialog >
{
{ x = > x . PluginId , plugin . Id } ,
{ x = > x . PluginLocalPath , plugin . LocalPath } ,
} ;
var dialogReference = await this . DialogService . ShowAsync < AssistantPluginEditorDialog > ( this . T ( "Edit Assistant Plugin" ) , parameters , DialogOptions . BLOCKING_FULLSCREEN ) ;
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null | | dialogResult . Canceled | | dialogResult . Data is not AssistantPluginEditorDialogResult result )
return ;
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." ) ;
2026-08-06 08:42:20 +00:00
// Saving the plugin ran LoadAll, which already sent PLUGINS_RELOADED. Editing the plugin
// code changes no settings, so there is nothing else to announce:
2026-07-15 19:00:25 +00:00
await this . InvokeAsync ( this . StateHasChanged ) ;
}
private async Task OpenAssistantPluginRevisionDialogAsync ( IAvailablePlugin plugin )
{
var parameters = new DialogParameters < AssistantPluginRevisionDialog >
{
{ x = > x . PluginId , plugin . Id } ,
{ x = > x . PluginLocalPath , plugin . LocalPath } ,
} ;
var dialogReference = await this . DialogService . ShowAsync < AssistantPluginRevisionDialog > ( this . T ( "Revise Assistant Plugin" ) , parameters , DialogOptions . BLOCKING_FULLSCREEN ) ;
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null | | dialogResult . Canceled | | dialogResult . Data is not AssistantPluginRevisionDialogResult result )
return ;
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." ) ;
2026-08-06 08:42:20 +00:00
// 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:
2026-07-15 19:00:25 +00:00
await this . MessageBus . SendMessage < bool > ( this , Event . CONFIGURATION_CHANGED ) ;
await this . InvokeAsync ( this . StateHasChanged ) ;
}
2026-08-06 08:42:20 +00:00
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 ;
2026-08-09 17:01:58 +00:00
var selection = await this . RustService . SelectFile ( this . T ( "Import plugin" ) , [ FileTypes . PLUGIN_ARCHIVE ] ) ;
2026-08-06 08:42:20 +00:00
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
{
2026-08-09 17:01:58 +00:00
var result = await this . PluginInstallService . InstallArchiveAsync ( archivePath , this . ConfirmPluginImportAsync , CancellationToken . None ) ;
2026-08-06 08:42:20 +00:00
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
2026-08-09 17:01:58 +00:00
? this . T ( "Plugin updated." )
: this . T ( "Plugin installed." ) ;
2026-08-06 08:42:20 +00:00
// 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 ;
}
2026-02-10 14:23:56 +00:00
private static bool IsSendingMail ( string sourceUrl ) = > sourceUrl . TrimStart ( ) . StartsWith ( "mailto:" , StringComparison . OrdinalIgnoreCase ) ;
2025-04-24 11:50:14 +00:00
2026-04-09 08:01:24 +00:00
private PluginAssistants ? TryGetAssistantPlugin ( Guid pluginId ) = > PluginFactory . RunningPlugins . OfType < PluginAssistants > ( ) . FirstOrDefault ( x = > x . Id = = pluginId ) ;
private async Task TryAutoAuditAssistantsAsync ( )
{
if ( this . isAutoAuditing | | ! this . AssistantPluginAuditSettings . AutomaticallyAuditAssistants )
return ;
this . isAutoAuditing = true ;
try
{
var wasConfigurationChanged = false ;
var assistantPlugins = PluginFactory . RunningPlugins . OfType < PluginAssistants > ( ) . ToList ( ) ;
foreach ( var assistantPlugin in assistantPlugins )
{
var securityState = PluginAssistantSecurityResolver . Resolve ( this . SettingsManager , assistantPlugin ) ;
if ( ! securityState . RequiresAudit )
continue ;
var audit = await this . AssistantPluginAuditService . RunAuditAsync ( assistantPlugin ) ;
if ( audit . Level is AssistantAuditLevel . UNKNOWN )
{
await MessageBus . INSTANCE . SendError ( new ( Icons . Material . Filled . SettingsSuggest , string . Format ( this . T ( "The automatic security audit for the assistant plugin '{0}' failed. Please run it manually." ) , assistantPlugin . Name ) ) ) ;
continue ;
}
this . UpsertAuditCard ( audit ) ;
wasConfigurationChanged = true ;
}
if ( ! wasConfigurationChanged )
return ;
await this . SettingsManager . StoreSettings ( ) ;
await this . MessageBus . SendMessage < bool > ( this , Event . CONFIGURATION_CHANGED ) ;
}
finally
{
this . isAutoAuditing = false ;
await this . InvokeAsync ( this . StateHasChanged ) ;
}
}
private void UpsertAuditCard ( PluginAssistantAudit audit )
{
var audits = this . SettingsManager . ConfigurationData . AssistantPluginAudits ;
var existingIndex = audits . FindIndex ( x = > x . PluginId = = audit . PluginId ) ;
if ( existingIndex > = 0 )
audits [ existingIndex ] = audit ;
else
audits . Add ( audit ) ;
}
2025-04-24 11:50:14 +00:00
#region Overrides of MSGComponentBase
protected override async Task ProcessIncomingMessage < T > ( ComponentBase ? sendingComponent , Event triggeredEvent , T ? data ) where T : default
{
switch ( triggeredEvent )
{
case Event . PLUGINS_RELOADED :
2026-04-09 08:01:24 +00:00
await this . TryAutoAuditAssistantsAsync ( ) ;
await this . InvokeAsync ( this . StateHasChanged ) ;
break ;
case Event . CONFIGURATION_CHANGED :
2025-04-24 11:50:14 +00:00
await this . InvokeAsync ( this . StateHasChanged ) ;
break ;
2026-08-06 08:42:20 +00:00
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 ;
2025-04-24 11:50:14 +00:00
}
}
#endregion
2026-08-06 08:42:20 +00:00
/// <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 ] ) ;
}
}