mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-24 20:52:11 +00:00
Added a plugin archive export for Linux
This commit is contained in:
parent
0b459801d4
commit
b45fb83cb0
@ -118,8 +118,8 @@
|
|||||||
|
|
||||||
@if (context is IAvailablePlugin shareablePlugin && CanSharePlugin(shareablePlugin))
|
@if (context is IAvailablePlugin shareablePlugin && CanSharePlugin(shareablePlugin))
|
||||||
{
|
{
|
||||||
<MudTooltip Text="@T("Share plugin archive")">
|
<MudTooltip Text="@this.SharePluginTooltip">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.IosShare" Size="Size.Medium" OnClick="@(() => this.SharePluginAsync(shareablePlugin))" Disabled="@(!CanSharePlugin(shareablePlugin))"/>
|
<MudIconButton Icon="@SharePluginIcon" Size="Size.Medium" OnClick="@(() => this.SharePluginAsync(shareablePlugin))" Disabled="@(!CanSharePlugin(shareablePlugin))"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -214,6 +214,14 @@ public partial class Plugins : MSGComponentBase
|
|||||||
|
|
||||||
private static bool CanSharePlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, IsManagedByConfigServer: false } && !string.IsNullOrWhiteSpace(plugin.LocalPath) && !IS_SHARING_PLUGIN;
|
private static bool CanSharePlugin(IAvailablePlugin plugin) => plugin is { IsInternal: false, IsManagedByConfigServer: false } && !string.IsNullOrWhiteSpace(plugin.LocalPath) && !IS_SHARING_PLUGIN;
|
||||||
|
|
||||||
|
/// <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 async Task OpenAssistantPluginEditorDialogAsync(IAvailablePlugin plugin)
|
private async Task OpenAssistantPluginEditorDialogAsync(IAvailablePlugin plugin)
|
||||||
{
|
{
|
||||||
var parameters = new DialogParameters<AssistantPluginEditorDialog>
|
var parameters = new DialogParameters<AssistantPluginEditorDialog>
|
||||||
@ -265,12 +273,19 @@ public partial class Plugins : MSGComponentBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var shareResult = await this.PluginShareService.ShareAsync(plugin, CancellationToken.None);
|
var shareResult = await this.PluginShareService.ShareAsync(plugin, CancellationToken.None);
|
||||||
|
if (shareResult.Cancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!shareResult.Success)
|
if (!shareResult.Success)
|
||||||
{
|
{
|
||||||
LOG.LogError($"Sharing the plugin '{shareResult.PluginName}' from archive '{shareResult.ArchivePath}' failed with Issue: '{shareResult.Issue}'.");
|
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, T("An error occurred while sharing the plugin.")));
|
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;
|
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
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using AIStudio.Tools.PluginSystem;
|
using AIStudio.Tools.PluginSystem;
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
|
||||||
namespace AIStudio.Tools.Services;
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
public sealed record PluginShareResult(bool Success, string PluginName, string ArchivePath, string Issue);
|
public sealed record PluginShareResult(bool Success, string PluginName, string ArchivePath, string Issue, bool Cancelled = false);
|
||||||
|
|
||||||
public sealed class PluginShareService(NativeShareService nativeShareService, ILogger<PluginShareService> logger)
|
public sealed class PluginShareService(NativeShareService nativeShareService, RustService rustService, ILogger<PluginShareService> logger)
|
||||||
{
|
{
|
||||||
private static PluginShareResult ShareError(IAvailablePlugin plugin, string issue) => new(false, plugin.Name, string.Empty, issue);
|
private static PluginShareResult ShareError(IAvailablePlugin plugin, string issue) => new(false, plugin.Name, string.Empty, issue);
|
||||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginShareService).Namespace, nameof(PluginShareService));
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginShareService).Namespace, nameof(PluginShareService));
|
||||||
@ -19,12 +20,17 @@ public sealed class PluginShareService(NativeShareService nativeShareService, IL
|
|||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a shareable plugin archive from a local plugin and opens the native share sheet.
|
/// Creates a shareable plugin archive from a local plugin and hands it over to the user.
|
||||||
/// The archive contains the plugin root contents, so <c>plugin.lua</c> is located at the archive root.
|
/// The archive contains the plugin root contents, so <c>plugin.lua</c> is located at the archive root.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// On Windows and macOS, the archive is created in a temporary directory and handed over to the
|
||||||
|
/// native share sheet. Linux has no such share sheet, since the XDG desktop portals do not provide
|
||||||
|
/// a share interface. Thus, the archive is exported to a location of the user's choice there.
|
||||||
|
/// </remarks>
|
||||||
/// <param name="plugin">The local plugin to archive and share.</param>
|
/// <param name="plugin">The local plugin to archive and share.</param>
|
||||||
/// <param name="token">Cancellation token for archive creation.</param>
|
/// <param name="token">Cancellation token for archive creation.</param>
|
||||||
/// <returns>The share result, including the retained temporary archive path when successful.</returns>
|
/// <returns>The share result, including the archive path when successful.</returns>
|
||||||
public async Task<PluginShareResult> ShareAsync(IAvailablePlugin plugin, CancellationToken token)
|
public async Task<PluginShareResult> ShareAsync(IAvailablePlugin plugin, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (plugin.IsInternal)
|
if (plugin.IsInternal)
|
||||||
@ -36,6 +42,67 @@ public sealed class PluginShareService(NativeShareService nativeShareService, IL
|
|||||||
if (!TryGetPluginRoot(plugin, out var pluginRoot, out var issue))
|
if (!TryGetPluginRoot(plugin, out var pluginRoot, out var issue))
|
||||||
return ShareError(plugin, issue);
|
return ShareError(plugin, issue);
|
||||||
|
|
||||||
|
if (OperatingSystem.IsLinux())
|
||||||
|
return await this.ExportAsync(plugin, pluginRoot, token);
|
||||||
|
|
||||||
|
return await this.ShareViaNativeSheetAsync(plugin, pluginRoot, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asks the user for a target location and writes the plugin archive to it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plugin">The local plugin to archive.</param>
|
||||||
|
/// <param name="pluginRoot">The validated plugin root directory.</param>
|
||||||
|
/// <param name="token">Cancellation token for archive creation.</param>
|
||||||
|
/// <returns>The share result, including the chosen archive path when successful.</returns>
|
||||||
|
private async Task<PluginShareResult> ExportAsync(IAvailablePlugin plugin, string pluginRoot, CancellationToken token)
|
||||||
|
{
|
||||||
|
var suggestedFileName = $"{CreateSafeFileNamePrefix(plugin.Name)}{PLUGIN_FILE_EXTENSION}";
|
||||||
|
var saveResponse = await rustService.SaveFile(TB("Export plugin archive"), [FileTypes.PLUGIN_ARCHIVE], suggestedFileName);
|
||||||
|
if (saveResponse.UserCancelled)
|
||||||
|
return new(false, plugin.Name, string.Empty, string.Empty, true);
|
||||||
|
|
||||||
|
var archivePath = saveResponse.SaveFilePath;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// The save dialog already asked the user about overwriting an existing file.
|
||||||
|
// ZipFile.CreateFromDirectory would fail on an existing file, though:
|
||||||
|
if (File.Exists(archivePath))
|
||||||
|
File.Delete(archivePath);
|
||||||
|
|
||||||
|
ZipFile.CreateFromDirectory(pluginRoot, archivePath, CompressionLevel.Optimal, false);
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
logger.LogInformation("Exported plugin '{PluginName}' ({PluginId}) to the archive '{ArchivePath}'.", plugin.Name, plugin.Id, archivePath);
|
||||||
|
return new(true, plugin.Name, archivePath, string.Empty);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
this.TryDeleteArchive(archivePath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
this.TryDeleteArchive(archivePath);
|
||||||
|
logger.LogError(exception, "Failed to export plugin '{PluginName}' ({PluginId}).", plugin.Name, plugin.Id);
|
||||||
|
return ShareError(plugin, string.Format(TB("Unexpected error: {0}"), exception.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the plugin archive in a temporary directory and opens the native share sheet for it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plugin">The local plugin to archive and share.</param>
|
||||||
|
/// <param name="pluginRoot">The validated plugin root directory.</param>
|
||||||
|
/// <param name="token">Cancellation token for archive creation.</param>
|
||||||
|
/// <returns>The share result, including the retained temporary archive path when successful.</returns>
|
||||||
|
private async Task<PluginShareResult> ShareViaNativeSheetAsync(IAvailablePlugin plugin, string pluginRoot, CancellationToken token)
|
||||||
|
{
|
||||||
var archiveDirectory = Path.Join(Path.GetTempPath(), TEMPORARY_ARCHIVE_DIRECTORY);
|
var archiveDirectory = Path.Join(Path.GetTempPath(), TEMPORARY_ARCHIVE_DIRECTORY);
|
||||||
var archivePath = Path.Join(archiveDirectory, $"{CreateSafeFileNamePrefix(plugin.Name)}-{plugin.Id:N}-{Guid.NewGuid():N}{PLUGIN_FILE_EXTENSION}");
|
var archivePath = Path.Join(archiveDirectory, $"{CreateSafeFileNamePrefix(plugin.Name)}-{plugin.Id:N}-{Guid.NewGuid():N}{PLUGIN_FILE_EXTENSION}");
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# v26.8.1, build 251 (2026-08-xx xx:xx UTC)
|
# v26.8.1, build 251 (2026-08-xx xx:xx UTC)
|
||||||
- Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings.
|
- Added a prototype Visual Briefing Assistant that turns documents, data, images, audio, and video into self-contained interactive HTML briefings.
|
||||||
- Added organization-configurable defaults and visibility controls for the Visual Briefing Assistant.
|
- Added organization-configurable defaults and visibility controls for the Visual Briefing Assistant.
|
||||||
- Added a share button for plugins, which uses the native share dialog on Windows and macOS.
|
- Added a share button for plugins, which uses the native share dialog on Windows and macOS. For Linux, we added an export option for plugins, which stores the plugin archive at a location of your choice.
|
||||||
- Added an import button on the plugins page to install plugin archives directly from your disk.
|
- Added an import button on the plugins page to install plugin archives directly from your disk.
|
||||||
- Added the dedicated file extension `.mwplugin` for plugin archives.
|
- Added the dedicated file extension `.mwplugin` for plugin archives.
|
||||||
- Upgraded dependencies to their latest versions to improve security and stability.
|
- Upgraded dependencies to their latest versions to improve security and stability.
|
||||||
@ -344,15 +344,6 @@ pub async fn open_path_in_file_manager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub(crate) async fn open_existing_file_in_file_manager(path: PathBuf) -> Result<(), String> {
|
|
||||||
if !path.is_file() {
|
|
||||||
return Err(format!("The requested path is not an existing file: {}", path.to_string_lossy()));
|
|
||||||
}
|
|
||||||
|
|
||||||
open_file_manager_target(&path).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn open_file_manager_target(requested_path: &Path) -> Result<(), String> {
|
async fn open_file_manager_target(requested_path: &Path) -> Result<(), String> {
|
||||||
let Some(target) = resolve_file_manager_target(requested_path) else {
|
let Some(target) = resolve_file_manager_target(requested_path) else {
|
||||||
let issue = format!(
|
let issue = format!(
|
||||||
|
|||||||
@ -49,9 +49,11 @@ fn failure(issue: impl Into<String>) -> Json<ShareFileResponse> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Linux has no native share sheet: the XDG desktop portals do not provide a share interface. The
|
||||||
|
// app exports the file through the save dialog instead, hence this endpoint is not used on Linux:
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
async fn share_file_on_platform(path: PathBuf) -> Result<(), String> {
|
async fn share_file_on_platform(_path: PathBuf) -> Result<(), String> {
|
||||||
crate::file_actions::open_existing_file_in_file_manager(path).await
|
Err(String::from("The native share sheet is not available on Linux."))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user