mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-07-21 17:06:27 +00:00
Improve Linux clipboard support and file handling (#872)
This commit is contained in:
parent
f666760415
commit
8fa0c4db01
@ -102,13 +102,14 @@ public partial class AttachDocuments : MSGComponentBase
|
|||||||
private uint numDropAreasAboveThis;
|
private uint numDropAreasAboveThis;
|
||||||
private bool isComponentHovered;
|
private bool isComponentHovered;
|
||||||
private bool isDraggingOver;
|
private bool isDraggingOver;
|
||||||
|
private bool isFileDialogOpen;
|
||||||
private MediaImportOwner EffectiveImportOwner => this.OwnerChat is not null
|
private MediaImportOwner EffectiveImportOwner => this.OwnerChat is not null
|
||||||
? MediaImportOwner.ForChat(this.OwnerChat.ChatId)
|
? MediaImportOwner.ForChat(this.OwnerChat.ChatId)
|
||||||
: this.ImportOwner ?? this.fallbackMediaImportOwner;
|
: this.ImportOwner ?? this.fallbackMediaImportOwner;
|
||||||
|
|
||||||
private MediaImportTarget EffectiveMediaImportTarget => new(this.EffectiveImportOwner, string.IsNullOrWhiteSpace(this.Name) ? "attachments" : this.Name);
|
private MediaImportTarget EffectiveMediaImportTarget => new(this.EffectiveImportOwner, string.IsNullOrWhiteSpace(this.Name) ? "attachments" : this.Name);
|
||||||
|
|
||||||
private bool IsUnavailable => this.Disabled || this.MediaTranscriptionService.IsBusy(this.EffectiveImportOwner);
|
private bool IsUnavailable => this.Disabled || this.isFileDialogOpen || this.MediaTranscriptionService.IsBusy(this.EffectiveImportOwner);
|
||||||
|
|
||||||
#region Overrides of MSGComponentBase
|
#region Overrides of MSGComponentBase
|
||||||
|
|
||||||
@ -310,13 +311,21 @@ public partial class AttachDocuments : MSGComponentBase
|
|||||||
if (this.IsUnavailable)
|
if (this.IsUnavailable)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var selectFiles = await this.RustService.SelectFiles(T("Select files to attach"));
|
this.isFileDialogOpen = true;
|
||||||
if (selectFiles.UserCancelled)
|
try
|
||||||
return;
|
{
|
||||||
|
var selectFiles = await this.RustService.SelectFiles(T("Select files to attach"));
|
||||||
|
if (selectFiles.UserCancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
await this.AddFileBatchAsync(selectFiles.SelectedFilePaths);
|
await this.AddFileBatchAsync(selectFiles.SelectedFilePaths);
|
||||||
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
|
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
|
||||||
await this.OnChange(this.DocumentPaths);
|
await this.OnChange(this.DocumentPaths);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.isFileDialogOpen = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenAttachmentsDialog()
|
private async Task OpenAttachmentsDialog()
|
||||||
@ -397,7 +406,17 @@ public partial class AttachDocuments : MSGComponentBase
|
|||||||
|
|
||||||
private async Task AddFileBatchAsync(IEnumerable<string> paths)
|
private async Task AddFileBatchAsync(IEnumerable<string> paths)
|
||||||
{
|
{
|
||||||
var existingPaths = paths.Where(File.Exists).ToList();
|
var pathList = paths.ToList();
|
||||||
|
var inaccessiblePaths = pathList.Where(path => !File.Exists(path)).ToList();
|
||||||
|
if (inaccessiblePaths.Count > 0)
|
||||||
|
{
|
||||||
|
this.Logger.LogWarning("Could not access {Count} dropped or selected file(s): {Paths}", inaccessiblePaths.Count, string.Join(", ", inaccessiblePaths));
|
||||||
|
await this.MessageBus.SendWarning(new(
|
||||||
|
Icons.Material.Filled.Warning,
|
||||||
|
this.T("Some files could not be accessed. Please select them with the file chooser instead.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingPaths = pathList.Except(inaccessiblePaths).ToList();
|
||||||
var mediaPaths = existingPaths.Where(IsTranscribableMedia).ToList();
|
var mediaPaths = existingPaths.Where(IsTranscribableMedia).ToList();
|
||||||
var regularPaths = existingPaths.Except(mediaPaths).ToList();
|
var regularPaths = existingPaths.Except(mediaPaths).ToList();
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
Color="Color.Primary"
|
Color="Color.Primary"
|
||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
Disabled="@this.IsDisabled"
|
Disabled="@(this.IsDisabled || this.isFileDialogOpen)"
|
||||||
Class="mb-1"
|
Class="mb-1"
|
||||||
OnClick="@this.OpenFileDialog">
|
OnClick="@this.OpenFileDialog">
|
||||||
@T("Choose File")
|
@T("Choose File")
|
||||||
|
|||||||
@ -49,6 +49,7 @@ public partial class ConfigurationFile : ConfigurationBaseCore
|
|||||||
private RustService RustService { get; init; } = null!;
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
private string internalText = string.Empty;
|
private string internalText = string.Empty;
|
||||||
|
private bool isFileDialogOpen;
|
||||||
private readonly Timer timer = new(TimeSpan.FromMilliseconds(500))
|
private readonly Timer timer = new(TimeSpan.FromMilliseconds(500))
|
||||||
{
|
{
|
||||||
AutoReset = false
|
AutoReset = false
|
||||||
@ -90,13 +91,24 @@ public partial class ConfigurationFile : ConfigurationBaseCore
|
|||||||
|
|
||||||
private async Task OpenFileDialog()
|
private async Task OpenFileDialog()
|
||||||
{
|
{
|
||||||
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.internalText) ? null : this.internalText);
|
if (this.isFileDialogOpen)
|
||||||
if (response.UserCancelled)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.timer.Stop();
|
this.isFileDialogOpen = true;
|
||||||
this.internalText = response.SelectedFilePath;
|
try
|
||||||
await this.OptionChanged(response.SelectedFilePath);
|
{
|
||||||
|
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.internalText) ? null : this.internalText);
|
||||||
|
if (response.UserCancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.timer.Stop();
|
||||||
|
this.internalText = response.SelectedFilePath;
|
||||||
|
await this.OptionChanged(response.SelectedFilePath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.isFileDialogOpen = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OptionChanged(string updatedText)
|
private async Task OptionChanged(string updatedText)
|
||||||
|
|||||||
@ -74,9 +74,10 @@ public partial class ReadFileContent : MSGComponentBase
|
|||||||
private string dragClass = DEFAULT_DRAG_CLASS;
|
private string dragClass = DEFAULT_DRAG_CLASS;
|
||||||
private uint numDropAreasAboveThis;
|
private uint numDropAreasAboveThis;
|
||||||
private bool isComponentHovered;
|
private bool isComponentHovered;
|
||||||
|
private bool isFileDialogOpen;
|
||||||
private bool IsCurrentTargetBusy => this.MediaTranscriptionService.GetSnapshot(this.EffectiveImportOwner) is { IsBusy: true } snapshot
|
private bool IsCurrentTargetBusy => this.MediaTranscriptionService.GetSnapshot(this.EffectiveImportOwner) is { IsBusy: true } snapshot
|
||||||
&& snapshot.Target == this.EffectiveMediaImportTarget;
|
&& snapshot.Target == this.EffectiveMediaImportTarget;
|
||||||
private bool IsUnavailable => this.Disabled || this.MediaTranscriptionService.IsBusy(this.EffectiveImportOwner);
|
private bool IsUnavailable => this.Disabled || this.isFileDialogOpen || this.MediaTranscriptionService.IsBusy(this.EffectiveImportOwner);
|
||||||
|
|
||||||
#region Overrides of MSGComponentBase
|
#region Overrides of MSGComponentBase
|
||||||
|
|
||||||
@ -217,14 +218,22 @@ public partial class ReadFileContent : MSGComponentBase
|
|||||||
if (this.IsUnavailable)
|
if (this.IsUnavailable)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var selectedFile = await this.RustService.SelectFile(T("Select file to read its content"));
|
this.isFileDialogOpen = true;
|
||||||
if (selectedFile.UserCancelled)
|
try
|
||||||
{
|
{
|
||||||
this.Logger.LogInformation("User cancelled the file selection");
|
var selectedFile = await this.RustService.SelectFile(T("Select file to read its content"));
|
||||||
return;
|
if (selectedFile.UserCancelled)
|
||||||
}
|
{
|
||||||
|
this.Logger.LogInformation("User cancelled the file selection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.LoadFileIfValid(selectedFile.SelectedFilePath);
|
await this.LoadFileIfValid(selectedFile.SelectedFilePath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.isFileDialogOpen = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> EnsurePandocAvailability()
|
private async Task<bool> EnsurePandocAvailability()
|
||||||
@ -246,6 +255,15 @@ public partial class ReadFileContent : MSGComponentBase
|
|||||||
|
|
||||||
private async Task LoadFirstValidFile(List<string> paths)
|
private async Task LoadFirstValidFile(List<string> paths)
|
||||||
{
|
{
|
||||||
|
var inaccessiblePaths = paths.Where(path => !File.Exists(path)).ToList();
|
||||||
|
if (inaccessiblePaths.Count > 0)
|
||||||
|
{
|
||||||
|
this.Logger.LogWarning("Could not access {Count} dropped file(s): {Paths}", inaccessiblePaths.Count, string.Join(", ", inaccessiblePaths));
|
||||||
|
await this.MessageBus.SendWarning(new(
|
||||||
|
Icons.Material.Filled.Warning,
|
||||||
|
this.T("Some dropped files could not be accessed. Please select them with the file chooser instead.")));
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var path in paths)
|
foreach (var path in paths)
|
||||||
{
|
{
|
||||||
if (await this.LoadFileIfValid(path))
|
if (await this.LoadFileIfValid(path))
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="this.Disabled" OnClick="@this.OpenDirectoryDialog">
|
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="@(this.Disabled || this.isDirectoryDialogOpen)" OnClick="@this.OpenDirectoryDialog">
|
||||||
@T("Choose Directory")
|
@T("Choose Directory")
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@ -31,6 +31,7 @@ public partial class SelectDirectory : MSGComponentBase
|
|||||||
protected ILogger<SelectDirectory> Logger { get; init; } = null!;
|
protected ILogger<SelectDirectory> Logger { get; init; } = null!;
|
||||||
|
|
||||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||||
|
private bool isDirectoryDialogOpen;
|
||||||
|
|
||||||
#region Overrides of ComponentBase
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
@ -51,10 +52,21 @@ public partial class SelectDirectory : MSGComponentBase
|
|||||||
|
|
||||||
private async Task OpenDirectoryDialog()
|
private async Task OpenDirectoryDialog()
|
||||||
{
|
{
|
||||||
var response = await this.RustService.SelectDirectory(this.DirectoryDialogTitle, string.IsNullOrWhiteSpace(this.Directory) ? null : this.Directory);
|
if (this.isDirectoryDialogOpen)
|
||||||
this.Logger.LogInformation($"The user selected the directory '{response.SelectedDirectory}'.");
|
return;
|
||||||
|
|
||||||
if (!response.UserCancelled)
|
this.isDirectoryDialogOpen = true;
|
||||||
this.InternalDirectoryChanged(response.SelectedDirectory);
|
try
|
||||||
|
{
|
||||||
|
var response = await this.RustService.SelectDirectory(this.DirectoryDialogTitle, string.IsNullOrWhiteSpace(this.Directory) ? null : this.Directory);
|
||||||
|
this.Logger.LogInformation("The user selected the directory '{SelectedDirectory}'.", response.SelectedDirectory);
|
||||||
|
|
||||||
|
if (!response.UserCancelled)
|
||||||
|
this.InternalDirectoryChanged(response.SelectedDirectory);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.isDirectoryDialogOpen = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="this.Disabled" OnClick="@this.OpenFileDialog">
|
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="@(this.Disabled || this.isFileDialogOpen)" OnClick="@this.OpenFileDialog">
|
||||||
@T("Choose File")
|
@T("Choose File")
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@ -35,6 +35,7 @@ public partial class SelectFile : MSGComponentBase
|
|||||||
protected ILogger<SelectFile> Logger { get; init; } = null!;
|
protected ILogger<SelectFile> Logger { get; init; } = null!;
|
||||||
|
|
||||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||||
|
private bool isFileDialogOpen;
|
||||||
|
|
||||||
#region Overrides of ComponentBase
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
@ -55,10 +56,21 @@ public partial class SelectFile : MSGComponentBase
|
|||||||
|
|
||||||
private async Task OpenFileDialog()
|
private async Task OpenFileDialog()
|
||||||
{
|
{
|
||||||
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.File) ? null : this.File);
|
if (this.isFileDialogOpen)
|
||||||
this.Logger.LogInformation($"The user selected the file '{response.SelectedFilePath}'.");
|
return;
|
||||||
|
|
||||||
if (!response.UserCancelled)
|
this.isFileDialogOpen = true;
|
||||||
this.InternalFileChanged(response.SelectedFilePath);
|
try
|
||||||
|
{
|
||||||
|
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.File) ? null : this.File);
|
||||||
|
this.Logger.LogInformation("The user selected the file '{SelectedFilePath}'.", response.SelectedFilePath);
|
||||||
|
|
||||||
|
if (!response.UserCancelled)
|
||||||
|
this.InternalFileChanged(response.SelectedFilePath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.isFileDialogOpen = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,7 +63,7 @@
|
|||||||
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))">
|
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))">
|
||||||
@T("Use shared attachment paths")
|
@T("Use shared attachment paths")
|
||||||
</MudMenuItem>
|
</MudMenuItem>
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.Folder" OnClick="@(() => this.ExportChatTemplateWithPackagedAttachments(context))">
|
<MudMenuItem Icon="@Icons.Material.Filled.Folder" Disabled="@this.isPluginDirectoryDialogOpen" OnClick="@(() => this.ExportChatTemplateWithPackagedAttachments(context))">
|
||||||
@T("Copy attachments into plugin")
|
@T("Copy attachments into plugin")
|
||||||
</MudMenuItem>
|
</MudMenuItem>
|
||||||
</MudMenu>
|
</MudMenu>
|
||||||
|
|||||||
@ -6,6 +6,8 @@ namespace AIStudio.Dialogs.Settings;
|
|||||||
|
|
||||||
public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||||
{
|
{
|
||||||
|
private bool isPluginDirectoryDialogOpen;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool CreateTemplateFromExistingChatThread { get; set; }
|
public bool CreateTemplateFromExistingChatThread { get; set; }
|
||||||
|
|
||||||
@ -131,7 +133,7 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
|||||||
|
|
||||||
private async Task ExportChatTemplateWithPackagedAttachments(ChatTemplate chatTemplate)
|
private async Task ExportChatTemplateWithPackagedAttachments(ChatTemplate chatTemplate)
|
||||||
{
|
{
|
||||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings || this.isPluginDirectoryDialogOpen)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
|
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
|
||||||
@ -143,11 +145,19 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pluginDirectoryResponse = await this.RustService.SelectDirectory(T("Select configuration plugin folder"));
|
this.isPluginDirectoryDialogOpen = true;
|
||||||
if (pluginDirectoryResponse.UserCancelled)
|
try
|
||||||
return;
|
{
|
||||||
|
var pluginDirectoryResponse = await this.RustService.SelectDirectory(T("Select configuration plugin folder"));
|
||||||
|
if (pluginDirectoryResponse.UserCancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
await this.CopyPackagedChatTemplateLuaToClipboard(chatTemplate, pluginDirectoryResponse.SelectedDirectory);
|
await this.CopyPackagedChatTemplateLuaToClipboard(chatTemplate, pluginDirectoryResponse.SelectedDirectory);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.isPluginDirectoryDialogOpen = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CopyChatTemplateLuaToClipboard(ChatTemplate chatTemplate)
|
private async Task CopyChatTemplateLuaToClipboard(ChatTemplate chatTemplate)
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
using AIStudio.Tools.Rust;
|
using AIStudio.Tools.Rust;
|
||||||
|
|
||||||
namespace AIStudio.Tools.Services;
|
namespace AIStudio.Tools.Services;
|
||||||
@ -6,56 +8,90 @@ public sealed partial class RustService
|
|||||||
{
|
{
|
||||||
public async Task<DirectorySelectionResponse> SelectDirectory(string title, string? initialDirectory = null)
|
public async Task<DirectorySelectionResponse> SelectDirectory(string title, string? initialDirectory = null)
|
||||||
{
|
{
|
||||||
var encodedTitle = Uri.EscapeDataString(title);
|
return await this.RunFileDialog(
|
||||||
var result = initialDirectory is null
|
"select directory",
|
||||||
? await this.http.PostAsync($"/select/directory?title={encodedTitle}", null)
|
async () =>
|
||||||
: await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", new PreviousDirectory(initialDirectory), this.jsonRustSerializerOptions);
|
{
|
||||||
|
var encodedTitle = Uri.EscapeDataString(title);
|
||||||
if (!result.IsSuccessStatusCode)
|
var result = initialDirectory is null
|
||||||
|
? await this.http.PostAsync($"/select/directory?title={encodedTitle}", null)
|
||||||
|
: await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", new PreviousDirectory(initialDirectory), this.jsonRustSerializerOptions);
|
||||||
|
|
||||||
|
if (result.IsSuccessStatusCode)
|
||||||
|
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions);
|
||||||
|
|
||||||
|
this.logger!.LogError("Failed to select a directory: '{StatusCode}'", result.StatusCode);
|
||||||
|
return new DirectorySelectionResponse(true, string.Empty);
|
||||||
|
},
|
||||||
|
new DirectorySelectionResponse(true, string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<T> RunFileDialog<T>(string operation, Func<Task<T>> showDialog, T cancelledResult)
|
||||||
|
{
|
||||||
|
if (!await this.fileDialogLock.WaitAsync(0))
|
||||||
{
|
{
|
||||||
this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'");
|
this.logger!.LogInformation("Ignored duplicate file dialog request for '{Operation}'.", operation);
|
||||||
return new DirectorySelectionResponse(true, string.Empty);
|
return cancelledResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
this.logger!.LogInformation("Opening file dialog for '{Operation}'.", operation);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await showDialog();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
this.fileDialogLock.Release();
|
||||||
|
this.logger!.LogInformation("File dialog for '{Operation}' completed after {ElapsedMilliseconds} ms.", operation, stopwatch.ElapsedMilliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<FileSelectionResponse> SelectFile(string title, FileTypeFilter[]? filter = null, string? initialFile = null)
|
public async Task<FileSelectionResponse> SelectFile(string title, FileTypeFilter[]? filter = null, string? initialFile = null)
|
||||||
{
|
{
|
||||||
var payload = new SelectFileOptions
|
return await this.RunFileDialog(
|
||||||
{
|
"select file",
|
||||||
Title = title,
|
async () =>
|
||||||
PreviousFile = initialFile is null ? null : new (initialFile),
|
{
|
||||||
Filter = FileTypes.AsOneFileType(filter)
|
var payload = new SelectFileOptions
|
||||||
};
|
{
|
||||||
|
Title = title,
|
||||||
|
PreviousFile = initialFile is null ? null : new (initialFile),
|
||||||
|
Filter = FileTypes.AsOneFileType(filter)
|
||||||
|
};
|
||||||
|
|
||||||
var result = await this.http.PostAsJsonAsync("/select/file", payload, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/select/file", payload, this.jsonRustSerializerOptions);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (result.IsSuccessStatusCode)
|
||||||
{
|
return await result.Content.ReadFromJsonAsync<FileSelectionResponse>(this.jsonRustSerializerOptions);
|
||||||
this.logger!.LogError($"Failed to select a file: '{result.StatusCode}'");
|
|
||||||
return new FileSelectionResponse(true, string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await result.Content.ReadFromJsonAsync<FileSelectionResponse>(this.jsonRustSerializerOptions);
|
this.logger!.LogError("Failed to select a file: '{StatusCode}'", result.StatusCode);
|
||||||
|
return new FileSelectionResponse(true, string.Empty);
|
||||||
|
},
|
||||||
|
new FileSelectionResponse(true, string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<FilesSelectionResponse> SelectFiles(string title, FileTypeFilter[]? filter = null, string? initialFile = null)
|
public async Task<FilesSelectionResponse> SelectFiles(string title, FileTypeFilter[]? filter = null, string? initialFile = null)
|
||||||
{
|
{
|
||||||
var payload = new SelectFileOptions
|
return await this.RunFileDialog(
|
||||||
{
|
"select files",
|
||||||
Title = title,
|
async () =>
|
||||||
PreviousFile = initialFile is null ? null : new (initialFile),
|
{
|
||||||
Filter = FileTypes.AsOneFileType(filter)
|
var payload = new SelectFileOptions
|
||||||
};
|
{
|
||||||
|
Title = title,
|
||||||
|
PreviousFile = initialFile is null ? null : new (initialFile),
|
||||||
|
Filter = FileTypes.AsOneFileType(filter)
|
||||||
|
};
|
||||||
|
|
||||||
var result = await this.http.PostAsJsonAsync("/select/files", payload, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/select/files", payload, this.jsonRustSerializerOptions);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (result.IsSuccessStatusCode)
|
||||||
{
|
return await result.Content.ReadFromJsonAsync<FilesSelectionResponse>(this.jsonRustSerializerOptions);
|
||||||
this.logger!.LogError($"Failed to select files: '{result.StatusCode}'");
|
|
||||||
return new FilesSelectionResponse(true, Array.Empty<string>());
|
|
||||||
}
|
|
||||||
|
|
||||||
return await result.Content.ReadFromJsonAsync<FilesSelectionResponse>(this.jsonRustSerializerOptions);
|
this.logger!.LogError("Failed to select files: '{StatusCode}'", result.StatusCode);
|
||||||
|
return new FilesSelectionResponse(true, Array.Empty<string>());
|
||||||
|
},
|
||||||
|
new FilesSelectionResponse(true, Array.Empty<string>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -68,21 +104,25 @@ public sealed partial class RustService
|
|||||||
/// operation and whether the select operation was successful.</returns>
|
/// operation and whether the select operation was successful.</returns>
|
||||||
public async Task<FileSaveResponse> SaveFile(string title, FileTypeFilter[]? filter = null, string? initialFile = null)
|
public async Task<FileSaveResponse> SaveFile(string title, FileTypeFilter[]? filter = null, string? initialFile = null)
|
||||||
{
|
{
|
||||||
var payload = new SaveFileOptions
|
return await this.RunFileDialog(
|
||||||
{
|
"save file",
|
||||||
Title = title,
|
async () =>
|
||||||
PreviousFile = initialFile is null ? null : new (initialFile),
|
{
|
||||||
Filter = FileTypes.AsOneFileType(filter)
|
var payload = new SaveFileOptions
|
||||||
};
|
{
|
||||||
|
Title = title,
|
||||||
var result = await this.http.PostAsJsonAsync("/save/file", payload, this.jsonRustSerializerOptions);
|
PreviousFile = initialFile is null ? null : new (initialFile),
|
||||||
if (!result.IsSuccessStatusCode)
|
Filter = FileTypes.AsOneFileType(filter)
|
||||||
{
|
};
|
||||||
this.logger!.LogError($"Failed to select a file for writing operation '{result.StatusCode}'");
|
|
||||||
return new FileSaveResponse(true, string.Empty);
|
var result = await this.http.PostAsJsonAsync("/save/file", payload, this.jsonRustSerializerOptions);
|
||||||
}
|
if (result.IsSuccessStatusCode)
|
||||||
|
return await result.Content.ReadFromJsonAsync<FileSaveResponse>(this.jsonRustSerializerOptions);
|
||||||
return await result.Content.ReadFromJsonAsync<FileSaveResponse>(this.jsonRustSerializerOptions);
|
|
||||||
|
this.logger!.LogError("Failed to select a file for writing operation: '{StatusCode}'", result.StatusCode);
|
||||||
|
return new FileSaveResponse(true, string.Empty);
|
||||||
|
},
|
||||||
|
new FileSaveResponse(true, string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OpenPathResponse> TryOpenPathInRuntimeFileManager(string path)
|
public async Task<OpenPathResponse> TryOpenPathInRuntimeFileManager(string path)
|
||||||
|
|||||||
@ -17,6 +17,7 @@ public sealed partial class RustService : BackgroundService
|
|||||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(RustService).Namespace, nameof(RustService));
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(RustService).Namespace, nameof(RustService));
|
||||||
|
|
||||||
private readonly HttpClient http;
|
private readonly HttpClient http;
|
||||||
|
private readonly SemaphoreSlim fileDialogLock = new(1, 1);
|
||||||
private readonly SemaphoreSlim userLanguageLock = new(1, 1);
|
private readonly SemaphoreSlim userLanguageLock = new(1, 1);
|
||||||
private readonly SemaphoreSlim userNameLock = new(1, 1);
|
private readonly SemaphoreSlim userNameLock = new(1, 1);
|
||||||
|
|
||||||
|
|||||||
@ -8,12 +8,14 @@
|
|||||||
- Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text or use documents alone when asking to identify tasks.
|
- Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text or use documents alone when asking to identify tasks.
|
||||||
- Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department.
|
- Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department.
|
||||||
- Improved secure API-key storage diagnostics on Linux. AI Studio now provides specific guidance when the default password collection is missing or locked, a password-manager prompt is dismissed, or no compatible Secret Service is available.
|
- Improved secure API-key storage diagnostics on Linux. AI Studio now provides specific guidance when the default password collection is missing or locked, a password-manager prompt is dismissed, or no compatible Secret Service is available.
|
||||||
|
- Improved the file dialogs to prevent opening multiple times when you click "Open" or "Save" multiple times in a row.
|
||||||
- Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep. Yes, we know this was an annoying bug, and we apologize for the inconvenience.
|
- Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep. Yes, we know this was an annoying bug, and we apologize for the inconvenience.
|
||||||
- Fixed connections to internal HTTPS services and enterprise configuration servers that use organization-provided root certificates on Linux.
|
- Fixed connections to internal HTTPS services and enterprise configuration servers that use organization-provided root certificates on Linux.
|
||||||
- Fixed enterprise configuration plugins from Windows-created ZIP files may not load correctly on Linux when the ZIP contained plugin files inside a folder.
|
- Fixed enterprise configuration plugins from Windows-created ZIP files may not load correctly on Linux when the ZIP contained plugin files inside a folder.
|
||||||
- Fixed the global voice recording shortcut on Linux so it also works outside AI Studio on supported Wayland desktops.
|
- Fixed the global voice recording shortcut on Linux so it also works outside AI Studio on supported Wayland desktops.
|
||||||
- Fixed voice recording and transcription on Linux.
|
- Fixed voice recording and transcription on Linux.
|
||||||
- Fixed copied content from AI Studio not remaining available on the clipboard on Linux.
|
- Fixed copied content from AI Studio not remaining available on the clipboard on Linux.
|
||||||
|
- Fixed dragging and dropping files from the home folder into the Linux Flatpak version.
|
||||||
- Fixed being able to switch document analysis policies while an analysis or media transcription was still in progress.
|
- Fixed being able to switch document analysis policies while an analysis or media transcription was still in progress.
|
||||||
- Fixed file extension handling so files are recognized correctly regardless of uppercase or lowercase letters in their extensions. Thanks, Paul Schweiß, for reporting this issue.
|
- Fixed file extension handling so files are recognized correctly regardless of uppercase or lowercase letters in their extensions. Thanks, Paul Schweiß, for reporting this issue.
|
||||||
- Fixed AI Studio failing to start on Linux systems & showing an outdated version on the Flatpak page.
|
- Fixed AI Studio failing to start on Linux systems & showing an outdated version on the Flatpak page.
|
||||||
|
|||||||
128
runtime/Cargo.lock
generated
128
runtime/Cargo.lock
generated
@ -149,7 +149,7 @@ version = "1.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -160,7 +160,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"once_cell_polyfill",
|
"once_cell_polyfill",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -215,6 +215,7 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
|
"wl-clipboard-rs",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -242,27 +243,6 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ashpd"
|
|
||||||
version = "0.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39"
|
|
||||||
dependencies = [
|
|
||||||
"enumflags2",
|
|
||||||
"futures-channel",
|
|
||||||
"futures-util",
|
|
||||||
"rand 0.9.4",
|
|
||||||
"raw-window-handle",
|
|
||||||
"serde",
|
|
||||||
"serde_repr",
|
|
||||||
"tokio",
|
|
||||||
"url",
|
|
||||||
"wayland-backend",
|
|
||||||
"wayland-client",
|
|
||||||
"wayland-protocols",
|
|
||||||
"zbus",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ashpd"
|
name = "ashpd"
|
||||||
version = "0.13.12"
|
version = "0.13.12"
|
||||||
@ -1885,7 +1865,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users",
|
"redox_users",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1911,15 +1891,6 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dlib"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
|
||||||
dependencies = [
|
|
||||||
"libloading 0.7.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dlopen2"
|
name = "dlopen2"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@ -2226,7 +2197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -4165,7 +4136,7 @@ dependencies = [
|
|||||||
"aes 0.9.1",
|
"aes 0.9.1",
|
||||||
"apple-native-keyring-store",
|
"apple-native-keyring-store",
|
||||||
"arboard",
|
"arboard",
|
||||||
"ashpd 0.13.12",
|
"ashpd",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-server",
|
"axum-server",
|
||||||
@ -4280,7 +4251,7 @@ dependencies = [
|
|||||||
"png 0.18.1",
|
"png 0.18.1",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5230,12 +5201,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pollster"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.13.1"
|
version = "1.13.1"
|
||||||
@ -5897,18 +5862,18 @@ version = "0.16.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ashpd 0.11.1",
|
|
||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"gtk-sys",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
"pollster",
|
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"urlencoding",
|
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@ -6080,7 +6045,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -6139,7 +6104,7 @@ dependencies = [
|
|||||||
"security-framework",
|
"security-framework",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
"webpki-root-certs",
|
"webpki-root-certs",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -6252,12 +6217,6 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scoped-tls"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -6748,7 +6707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -7675,10 +7634,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.4.2",
|
"getrandom 0.3.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -8137,7 +8096,19 @@ dependencies = [
|
|||||||
"png 0.18.1",
|
"png 0.18.1",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tree_magic_mini"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"once_cell",
|
||||||
|
"petgraph",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -8178,7 +8149,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"memoffset",
|
"memoffset",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -8286,12 +8257,6 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "urlencoding"
|
|
||||||
version = "2.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urlpattern"
|
name = "urlpattern"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@ -8639,7 +8604,6 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"downcast-rs",
|
"downcast-rs",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"scoped-tls",
|
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"wayland-sys",
|
"wayland-sys",
|
||||||
]
|
]
|
||||||
@ -8668,6 +8632,19 @@ dependencies = [
|
|||||||
"wayland-scanner",
|
"wayland-scanner",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wayland-protocols-wlr"
|
||||||
|
version = "0.3.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols",
|
||||||
|
"wayland-scanner",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-scanner"
|
name = "wayland-scanner"
|
||||||
version = "0.31.8"
|
version = "0.31.8"
|
||||||
@ -8685,8 +8662,6 @@ version = "0.31.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
|
checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dlib",
|
|
||||||
"log",
|
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -9574,6 +9549,24 @@ dependencies = [
|
|||||||
"wasmparser",
|
"wasmparser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wl-clipboard-rs"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"os_pipe",
|
||||||
|
"rustix 1.1.4",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tree_magic_mini",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols",
|
||||||
|
"wayland-protocols-wlr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "write16"
|
name = "write16"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@ -10054,7 +10047,6 @@ dependencies = [
|
|||||||
"endi",
|
"endi",
|
||||||
"enumflags2",
|
"enumflags2",
|
||||||
"serde",
|
"serde",
|
||||||
"url",
|
|
||||||
"winnow 1.0.2",
|
"winnow 1.0.2",
|
||||||
"zvariant_derive",
|
"zvariant_derive",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
|
|||||||
@ -12,13 +12,13 @@ tauri-build = { version = "2.6.3", features = [] }
|
|||||||
tauri = { version = "2.11.5", features = [] }
|
tauri = { version = "2.11.5", features = [] }
|
||||||
tauri-plugin-window-state = { version = "2.4.1" }
|
tauri-plugin-window-state = { version = "2.4.1" }
|
||||||
tauri-plugin-shell = "2.3.5"
|
tauri-plugin-shell = "2.3.5"
|
||||||
tauri-plugin-dialog = { version = "2.7.1", default-features = false, features = ["xdg-portal"] }
|
tauri-plugin-dialog = "2.7.1"
|
||||||
tauri-plugin-opener = "2.5.4"
|
tauri-plugin-opener = "2.5.4"
|
||||||
tauri-plugin-single-instance = "2"
|
tauri-plugin-single-instance = "2"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.150"
|
serde_json = "1.0.150"
|
||||||
keyring-core = "1.0.0"
|
keyring-core = "1.0.0"
|
||||||
arboard = "3.6.1"
|
arboard = { version = "3.6.1", features = ["wayland-data-control"] }
|
||||||
tokio = { version = "1.52.3", features = ["rt", "rt-multi-thread", "macros", "process"] }
|
tokio = { version = "1.52.3", features = ["rt", "rt-multi-thread", "macros", "process"] }
|
||||||
tokio-stream = { version = "0.1.18", features = ["sync"] }
|
tokio-stream = { version = "0.1.18", features = ["sync"] }
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
|
use axum::Json;
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, warn};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use axum::Json;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use crate::api_token::APIToken;
|
use crate::api_token::APIToken;
|
||||||
use crate::encryption::{EncryptedText, ENCRYPTION};
|
use crate::encryption::{EncryptedText, ENCRYPTION};
|
||||||
@ -26,17 +26,32 @@ impl ClipboardBackend for Clipboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
enum ClipboardOperationError<E> {
|
||||||
|
Initialization(E),
|
||||||
|
Write(E),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Display> Display for ClipboardOperationError<E> {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Initialization(error) => write!(formatter, "Failed to initialize the clipboard backend: {error}"),
|
||||||
|
Self::Write(error) => write!(formatter, "Failed to write to the clipboard: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn set_text_with_retry<B, F>(
|
fn set_text_with_retry<B, F>(
|
||||||
clipboard: &mut Option<B>,
|
clipboard: &mut Option<B>,
|
||||||
text: String,
|
text: String,
|
||||||
mut create_clipboard: F,
|
mut create_clipboard: F,
|
||||||
) -> Result<(), B::Error>
|
) -> Result<(), ClipboardOperationError<B::Error>>
|
||||||
where
|
where
|
||||||
B: ClipboardBackend,
|
B: ClipboardBackend,
|
||||||
F: FnMut() -> Result<B, B::Error>,
|
F: FnMut() -> Result<B, B::Error>,
|
||||||
{
|
{
|
||||||
if clipboard.is_none() {
|
if clipboard.is_none() {
|
||||||
*clipboard = Some(create_clipboard()?);
|
*clipboard = Some(create_clipboard().map_err(ClipboardOperationError::Initialization)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
let first_result = clipboard.as_mut().unwrap().set_text(text.clone());
|
let first_result = clipboard.as_mut().unwrap().set_text(text.clone());
|
||||||
@ -44,10 +59,10 @@ where
|
|||||||
warn!(Source = "Clipboard"; "Failed to set text using the current clipboard backend; reinitializing it once: {first_error}.");
|
warn!(Source = "Clipboard"; "Failed to set text using the current clipboard backend; reinitializing it once: {first_error}.");
|
||||||
*clipboard = None;
|
*clipboard = None;
|
||||||
|
|
||||||
let mut retry_clipboard = create_clipboard()?;
|
let mut retry_clipboard = create_clipboard().map_err(ClipboardOperationError::Initialization)?;
|
||||||
if let Err(retry_error) = retry_clipboard.set_text(text) {
|
if let Err(retry_error) = retry_clipboard.set_text(text) {
|
||||||
error!(Source = "Clipboard"; "Failed to set text after reinitializing the clipboard backend: {retry_error}.");
|
error!(Source = "Clipboard"; "Failed to set text after reinitializing the clipboard backend: {retry_error}.");
|
||||||
return Err(retry_error);
|
return Err(ClipboardOperationError::Write(retry_error));
|
||||||
}
|
}
|
||||||
|
|
||||||
*clipboard = Some(retry_clipboard);
|
*clipboard = Some(retry_clipboard);
|
||||||
@ -87,7 +102,7 @@ pub async fn set_clipboard(_token: APIToken, encrypted_text: String) -> Json<Set
|
|||||||
},
|
},
|
||||||
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(Source = "Clipboard"; "Failed to set text to the clipboard: {e}.");
|
error!(Source = "Clipboard"; "Clipboard operation failed: {e}.");
|
||||||
Json(SetClipboardResponse {
|
Json(SetClipboardResponse {
|
||||||
success: false,
|
success: false,
|
||||||
issue: e.to_string(),
|
issue: e.to_string(),
|
||||||
@ -116,7 +131,7 @@ mod tests {
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use super::{release_clipboard, set_text_with_retry, ClipboardBackend};
|
use super::{ClipboardOperationError, release_clipboard, set_text_with_retry, ClipboardBackend};
|
||||||
|
|
||||||
struct MockClipboard {
|
struct MockClipboard {
|
||||||
id: usize,
|
id: usize,
|
||||||
@ -186,6 +201,30 @@ mod tests {
|
|||||||
assert!(clipboard.is_some());
|
assert!(clipboard.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_initialization_failures_and_retries_on_the_next_request() {
|
||||||
|
let mut clipboard: Option<MockClipboard> = None;
|
||||||
|
let mut factory = MockFactory::new([false]);
|
||||||
|
let mut fail_initialization = true;
|
||||||
|
|
||||||
|
let error = set_text_with_retry(&mut clipboard, "first".to_string(), || {
|
||||||
|
if fail_initialization {
|
||||||
|
fail_initialization = false;
|
||||||
|
Err("initialization failed".to_string())
|
||||||
|
} else {
|
||||||
|
factory.create()
|
||||||
|
}
|
||||||
|
}).unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error, ClipboardOperationError::Initialization("initialization failed".to_string()));
|
||||||
|
assert!(clipboard.is_none());
|
||||||
|
|
||||||
|
set_text_with_retry(&mut clipboard, "second".to_string(), || factory.create()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(factory.created, 1);
|
||||||
|
assert!(clipboard.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reuses_the_same_instance_for_multiple_writes() {
|
fn reuses_the_same_instance_for_multiple_writes() {
|
||||||
let mut clipboard = None;
|
let mut clipboard = None;
|
||||||
@ -210,6 +249,26 @@ mod tests {
|
|||||||
assert_eq!(*factory.writes.lock().unwrap(), vec![(0, "text".to_string()), (1, "text".to_string())]);
|
assert_eq!(*factory.writes.lock().unwrap(), vec![(0, "text".to_string()), (1, "text".to_string())]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_reinitialization_failures_and_discards_the_failed_instance() {
|
||||||
|
let mut clipboard = None;
|
||||||
|
let mut factory = MockFactory::new([true]);
|
||||||
|
let mut initialization_attempts = 0;
|
||||||
|
|
||||||
|
let error = set_text_with_retry(&mut clipboard, "text".to_string(), || {
|
||||||
|
initialization_attempts += 1;
|
||||||
|
if initialization_attempts == 1 {
|
||||||
|
factory.create()
|
||||||
|
} else {
|
||||||
|
Err("reinitialization failed".to_string())
|
||||||
|
}
|
||||||
|
}).unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error, ClipboardOperationError::Initialization("reinitialization failed".to_string()));
|
||||||
|
assert_eq!(initialization_attempts, 2);
|
||||||
|
assert!(clipboard.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn returns_the_retry_error_and_discards_the_failed_instance() {
|
fn returns_the_retry_error_and_discards_the_failed_instance() {
|
||||||
let mut clipboard = None;
|
let mut clipboard = None;
|
||||||
@ -217,7 +276,7 @@ mod tests {
|
|||||||
|
|
||||||
let error = set_text_with_retry(&mut clipboard, "text".to_string(), || factory.create()).unwrap_err();
|
let error = set_text_with_retry(&mut clipboard, "text".to_string(), || factory.create()).unwrap_err();
|
||||||
|
|
||||||
assert_eq!(error, "backend 1 failed");
|
assert_eq!(error, ClipboardOperationError::Write("backend 1 failed".to_string()));
|
||||||
assert_eq!(factory.created, 2);
|
assert_eq!(factory.created, 2);
|
||||||
assert!(clipboard.is_none());
|
assert!(clipboard.is_none());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user