mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 18:32:12 +00:00
Prevent file dialogs from opening multiple times
This commit is contained in:
parent
f666760415
commit
7cb79dbc08
@ -102,13 +102,14 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
private uint numDropAreasAboveThis;
|
||||
private bool isComponentHovered;
|
||||
private bool isDraggingOver;
|
||||
private bool isFileDialogOpen;
|
||||
private MediaImportOwner EffectiveImportOwner => this.OwnerChat is not null
|
||||
? MediaImportOwner.ForChat(this.OwnerChat.ChatId)
|
||||
: this.ImportOwner ?? this.fallbackMediaImportOwner;
|
||||
|
||||
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
|
||||
|
||||
@ -310,6 +311,9 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
if (this.IsUnavailable)
|
||||
return;
|
||||
|
||||
this.isFileDialogOpen = true;
|
||||
try
|
||||
{
|
||||
var selectFiles = await this.RustService.SelectFiles(T("Select files to attach"));
|
||||
if (selectFiles.UserCancelled)
|
||||
return;
|
||||
@ -318,6 +322,11 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
|
||||
await this.OnChange(this.DocumentPaths);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isFileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenAttachmentsDialog()
|
||||
{
|
||||
@ -397,7 +406,17 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
|
||||
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 regularPaths = existingPaths.Except(mediaPaths).ToList();
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Disabled="@this.IsDisabled"
|
||||
Disabled="@(this.IsDisabled || this.isFileDialogOpen)"
|
||||
Class="mb-1"
|
||||
OnClick="@this.OpenFileDialog">
|
||||
@T("Choose File")
|
||||
|
||||
@ -49,6 +49,7 @@ public partial class ConfigurationFile : ConfigurationBaseCore
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
private string internalText = string.Empty;
|
||||
private bool isFileDialogOpen;
|
||||
private readonly Timer timer = new(TimeSpan.FromMilliseconds(500))
|
||||
{
|
||||
AutoReset = false
|
||||
@ -89,6 +90,12 @@ public partial class ConfigurationFile : ConfigurationBaseCore
|
||||
}
|
||||
|
||||
private async Task OpenFileDialog()
|
||||
{
|
||||
if (this.isFileDialogOpen)
|
||||
return;
|
||||
|
||||
this.isFileDialogOpen = true;
|
||||
try
|
||||
{
|
||||
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.internalText) ? null : this.internalText);
|
||||
if (response.UserCancelled)
|
||||
@ -98,6 +105,11 @@ public partial class ConfigurationFile : ConfigurationBaseCore
|
||||
this.internalText = response.SelectedFilePath;
|
||||
await this.OptionChanged(response.SelectedFilePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isFileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OptionChanged(string updatedText)
|
||||
{
|
||||
|
||||
@ -74,9 +74,10 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
private string dragClass = DEFAULT_DRAG_CLASS;
|
||||
private uint numDropAreasAboveThis;
|
||||
private bool isComponentHovered;
|
||||
private bool isFileDialogOpen;
|
||||
private bool IsCurrentTargetBusy => this.MediaTranscriptionService.GetSnapshot(this.EffectiveImportOwner) is { IsBusy: true } snapshot
|
||||
&& 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
|
||||
|
||||
@ -217,6 +218,9 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
if (this.IsUnavailable)
|
||||
return;
|
||||
|
||||
this.isFileDialogOpen = true;
|
||||
try
|
||||
{
|
||||
var selectedFile = await this.RustService.SelectFile(T("Select file to read its content"));
|
||||
if (selectedFile.UserCancelled)
|
||||
{
|
||||
@ -226,6 +230,11 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
|
||||
await this.LoadFileIfValid(selectedFile.SelectedFilePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isFileDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> EnsurePandocAvailability()
|
||||
{
|
||||
@ -246,6 +255,15 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
|
||||
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)
|
||||
{
|
||||
if (await this.LoadFileIfValid(path))
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
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")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@ -31,6 +31,7 @@ public partial class SelectDirectory : MSGComponentBase
|
||||
protected ILogger<SelectDirectory> Logger { get; init; } = null!;
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
private bool isDirectoryDialogOpen;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
@ -50,11 +51,22 @@ public partial class SelectDirectory : MSGComponentBase
|
||||
}
|
||||
|
||||
private async Task OpenDirectoryDialog()
|
||||
{
|
||||
if (this.isDirectoryDialogOpen)
|
||||
return;
|
||||
|
||||
this.isDirectoryDialogOpen = true;
|
||||
try
|
||||
{
|
||||
var response = await this.RustService.SelectDirectory(this.DirectoryDialogTitle, string.IsNullOrWhiteSpace(this.Directory) ? null : this.Directory);
|
||||
this.Logger.LogInformation($"The user selected the directory '{response.SelectedDirectory}'.");
|
||||
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"
|
||||
/>
|
||||
|
||||
<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")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@ -35,6 +35,7 @@ public partial class SelectFile : MSGComponentBase
|
||||
protected ILogger<SelectFile> Logger { get; init; } = null!;
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
private bool isFileDialogOpen;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
@ -54,11 +55,22 @@ public partial class SelectFile : MSGComponentBase
|
||||
}
|
||||
|
||||
private async Task OpenFileDialog()
|
||||
{
|
||||
if (this.isFileDialogOpen)
|
||||
return;
|
||||
|
||||
this.isFileDialogOpen = true;
|
||||
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 '{response.SelectedFilePath}'.");
|
||||
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))">
|
||||
@T("Use shared attachment paths")
|
||||
</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")
|
||||
</MudMenuItem>
|
||||
</MudMenu>
|
||||
|
||||
@ -6,6 +6,8 @@ namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
{
|
||||
private bool isPluginDirectoryDialogOpen;
|
||||
|
||||
[Parameter]
|
||||
public bool CreateTemplateFromExistingChatThread { get; set; }
|
||||
|
||||
@ -131,7 +133,7 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
|
||||
private async Task ExportChatTemplateWithPackagedAttachments(ChatTemplate chatTemplate)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings || this.isPluginDirectoryDialogOpen)
|
||||
return;
|
||||
|
||||
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
|
||||
@ -143,12 +145,20 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
return;
|
||||
}
|
||||
|
||||
this.isPluginDirectoryDialogOpen = true;
|
||||
try
|
||||
{
|
||||
var pluginDirectoryResponse = await this.RustService.SelectDirectory(T("Select configuration plugin folder"));
|
||||
if (pluginDirectoryResponse.UserCancelled)
|
||||
return;
|
||||
|
||||
await this.CopyPackagedChatTemplateLuaToClipboard(chatTemplate, pluginDirectoryResponse.SelectedDirectory);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isPluginDirectoryDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CopyChatTemplateLuaToClipboard(ChatTemplate chatTemplate)
|
||||
{
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools.Services;
|
||||
@ -5,22 +7,52 @@ namespace AIStudio.Tools.Services;
|
||||
public sealed partial class RustService
|
||||
{
|
||||
public async Task<DirectorySelectionResponse> SelectDirectory(string title, string? initialDirectory = null)
|
||||
{
|
||||
return await this.RunFileDialog(
|
||||
"select directory",
|
||||
async () =>
|
||||
{
|
||||
var encodedTitle = Uri.EscapeDataString(title);
|
||||
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)
|
||||
{
|
||||
this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'");
|
||||
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));
|
||||
}
|
||||
|
||||
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions);
|
||||
private async Task<T> RunFileDialog<T>(string operation, Func<Task<T>> showDialog, T cancelledResult)
|
||||
{
|
||||
if (!await this.fileDialogLock.WaitAsync(0))
|
||||
{
|
||||
this.logger!.LogInformation("Ignored duplicate file dialog request for '{Operation}'.", operation);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FileSelectionResponse> SelectFile(string title, FileTypeFilter[]? filter = null, string? initialFile = null)
|
||||
{
|
||||
return await this.RunFileDialog(
|
||||
"select file",
|
||||
async () =>
|
||||
{
|
||||
var payload = new SelectFileOptions
|
||||
{
|
||||
@ -30,16 +62,20 @@ public sealed partial class RustService
|
||||
};
|
||||
|
||||
var result = await this.http.PostAsJsonAsync("/select/file", payload, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to select a file: '{result.StatusCode}'");
|
||||
return new FileSelectionResponse(true, string.Empty);
|
||||
}
|
||||
|
||||
if (result.IsSuccessStatusCode)
|
||||
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)
|
||||
{
|
||||
return await this.RunFileDialog(
|
||||
"select files",
|
||||
async () =>
|
||||
{
|
||||
var payload = new SelectFileOptions
|
||||
{
|
||||
@ -49,13 +85,13 @@ public sealed partial class RustService
|
||||
};
|
||||
|
||||
var result = await this.http.PostAsJsonAsync("/select/files", payload, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to select files: '{result.StatusCode}'");
|
||||
return new FilesSelectionResponse(true, Array.Empty<string>());
|
||||
}
|
||||
|
||||
if (result.IsSuccessStatusCode)
|
||||
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>
|
||||
@ -67,6 +103,10 @@ public sealed partial class RustService
|
||||
/// <returns>A <see cref="FileSaveResponse"/> object containing information about whether the user canceled the
|
||||
/// operation and whether the select operation was successful.</returns>
|
||||
public async Task<FileSaveResponse> SaveFile(string title, FileTypeFilter[]? filter = null, string? initialFile = null)
|
||||
{
|
||||
return await this.RunFileDialog(
|
||||
"save file",
|
||||
async () =>
|
||||
{
|
||||
var payload = new SaveFileOptions
|
||||
{
|
||||
@ -76,13 +116,13 @@ public sealed partial class RustService
|
||||
};
|
||||
|
||||
var result = await this.http.PostAsJsonAsync("/save/file", payload, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to select a file for writing operation '{result.StatusCode}'");
|
||||
return new FileSaveResponse(true, string.Empty);
|
||||
}
|
||||
|
||||
if (result.IsSuccessStatusCode)
|
||||
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)
|
||||
|
||||
@ -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 readonly HttpClient http;
|
||||
private readonly SemaphoreSlim fileDialogLock = new(1, 1);
|
||||
private readonly SemaphoreSlim userLanguageLock = new(1, 1);
|
||||
private readonly SemaphoreSlim userNameLock = new(1, 1);
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
- 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 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 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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user