mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 20:12:12 +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))
|
||||
{
|
||||
<MudTooltip Text="@T("Share plugin archive")">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.IosShare" Size="Size.Medium" OnClick="@(() => this.SharePluginAsync(shareablePlugin))" Disabled="@(!CanSharePlugin(shareablePlugin))"/>
|
||||
<MudTooltip Text="@this.SharePluginTooltip">
|
||||
<MudIconButton Icon="@SharePluginIcon" Size="Size.Medium" OnClick="@(() => this.SharePluginAsync(shareablePlugin))" Disabled="@(!CanSharePlugin(shareablePlugin))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
|
||||
@ -213,7 +213,15 @@ public partial class Plugins : MSGComponentBase
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var parameters = new DialogParameters<AssistantPluginEditorDialog>
|
||||
@ -265,12 +273,19 @@ public partial class Plugins : MSGComponentBase
|
||||
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, 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;
|
||||
}
|
||||
|
||||
// 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
|
||||
{
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
using System.IO.Compression;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
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 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>
|
||||
/// 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.
|
||||
/// </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="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)
|
||||
{
|
||||
if (plugin.IsInternal)
|
||||
@ -36,6 +42,67 @@ public sealed class PluginShareService(NativeShareService nativeShareService, IL
|
||||
if (!TryGetPluginRoot(plugin, out var pluginRoot, out var 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 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)
|
||||
- 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 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 the dedicated file extension `.mwplugin` for plugin archives.
|
||||
- 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> {
|
||||
let Some(target) = resolve_file_manager_target(requested_path) else {
|
||||
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")]
|
||||
async fn share_file_on_platform(path: PathBuf) -> Result<(), String> {
|
||||
crate::file_actions::open_existing_file_in_file_manager(path).await
|
||||
async fn share_file_on_platform(_path: PathBuf) -> Result<(), String> {
|
||||
Err(String::from("The native share sheet is not available on Linux."))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user