mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 19:52:10 +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 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,6 +311,9 @@ public partial class AttachDocuments : MSGComponentBase
|
|||||||
if (this.IsUnavailable)
|
if (this.IsUnavailable)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
this.isFileDialogOpen = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
var selectFiles = await this.RustService.SelectFiles(T("Select files to attach"));
|
var selectFiles = await this.RustService.SelectFiles(T("Select files to attach"));
|
||||||
if (selectFiles.UserCancelled)
|
if (selectFiles.UserCancelled)
|
||||||
return;
|
return;
|
||||||
@ -318,6 +322,11 @@ public partial class AttachDocuments : MSGComponentBase
|
|||||||
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
|
||||||
@ -89,6 +90,12 @@ public partial class ConfigurationFile : ConfigurationBaseCore
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenFileDialog()
|
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);
|
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.internalText) ? null : this.internalText);
|
||||||
if (response.UserCancelled)
|
if (response.UserCancelled)
|
||||||
@ -98,6 +105,11 @@ public partial class ConfigurationFile : ConfigurationBaseCore
|
|||||||
this.internalText = response.SelectedFilePath;
|
this.internalText = response.SelectedFilePath;
|
||||||
await this.OptionChanged(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,6 +218,9 @@ public partial class ReadFileContent : MSGComponentBase
|
|||||||
if (this.IsUnavailable)
|
if (this.IsUnavailable)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
this.isFileDialogOpen = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
var selectedFile = await this.RustService.SelectFile(T("Select file to read its content"));
|
var selectedFile = await this.RustService.SelectFile(T("Select file to read its content"));
|
||||||
if (selectedFile.UserCancelled)
|
if (selectedFile.UserCancelled)
|
||||||
{
|
{
|
||||||
@ -226,6 +230,11 @@ public partial class ReadFileContent : MSGComponentBase
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
@ -50,11 +51,22 @@ public partial class SelectDirectory : MSGComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenDirectoryDialog()
|
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);
|
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)
|
if (!response.UserCancelled)
|
||||||
this.InternalDirectoryChanged(response.SelectedDirectory);
|
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
|
||||||
|
|
||||||
@ -54,11 +55,22 @@ public partial class SelectFile : MSGComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenFileDialog()
|
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);
|
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)
|
if (!response.UserCancelled)
|
||||||
this.InternalFileChanged(response.SelectedFilePath);
|
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,12 +145,20 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isPluginDirectoryDialogOpen = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
var pluginDirectoryResponse = await this.RustService.SelectDirectory(T("Select configuration plugin folder"));
|
var pluginDirectoryResponse = await this.RustService.SelectDirectory(T("Select configuration plugin folder"));
|
||||||
if (pluginDirectoryResponse.UserCancelled)
|
if (pluginDirectoryResponse.UserCancelled)
|
||||||
return;
|
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;
|
||||||
@ -5,22 +7,52 @@ namespace AIStudio.Tools.Services;
|
|||||||
public sealed partial class RustService
|
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)
|
||||||
|
{
|
||||||
|
return await this.RunFileDialog(
|
||||||
|
"select directory",
|
||||||
|
async () =>
|
||||||
{
|
{
|
||||||
var encodedTitle = Uri.EscapeDataString(title);
|
var encodedTitle = Uri.EscapeDataString(title);
|
||||||
var result = initialDirectory is null
|
var result = initialDirectory is null
|
||||||
? await this.http.PostAsync($"/select/directory?title={encodedTitle}", null)
|
? await this.http.PostAsync($"/select/directory?title={encodedTitle}", null)
|
||||||
: await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", new PreviousDirectory(initialDirectory), this.jsonRustSerializerOptions);
|
: await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", new PreviousDirectory(initialDirectory), this.jsonRustSerializerOptions);
|
||||||
|
|
||||||
if (!result.IsSuccessStatusCode)
|
if (result.IsSuccessStatusCode)
|
||||||
{
|
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions);
|
||||||
this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'");
|
|
||||||
|
this.logger!.LogError("Failed to select a directory: '{StatusCode}'", result.StatusCode);
|
||||||
return new DirectorySelectionResponse(true, string.Empty);
|
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)
|
public async Task<FileSelectionResponse> SelectFile(string title, FileTypeFilter[]? filter = null, string? initialFile = null)
|
||||||
|
{
|
||||||
|
return await this.RunFileDialog(
|
||||||
|
"select file",
|
||||||
|
async () =>
|
||||||
{
|
{
|
||||||
var payload = new SelectFileOptions
|
var payload = new SelectFileOptions
|
||||||
{
|
{
|
||||||
@ -30,16 +62,20 @@ public sealed partial class RustService
|
|||||||
};
|
};
|
||||||
|
|
||||||
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)
|
||||||
{
|
|
||||||
this.logger!.LogError($"Failed to select a file: '{result.StatusCode}'");
|
|
||||||
return new FileSelectionResponse(true, string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await result.Content.ReadFromJsonAsync<FileSelectionResponse>(this.jsonRustSerializerOptions);
|
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)
|
||||||
|
{
|
||||||
|
return await this.RunFileDialog(
|
||||||
|
"select files",
|
||||||
|
async () =>
|
||||||
{
|
{
|
||||||
var payload = new SelectFileOptions
|
var payload = new SelectFileOptions
|
||||||
{
|
{
|
||||||
@ -49,13 +85,13 @@ public sealed partial class RustService
|
|||||||
};
|
};
|
||||||
|
|
||||||
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)
|
||||||
{
|
|
||||||
this.logger!.LogError($"Failed to select files: '{result.StatusCode}'");
|
|
||||||
return new FilesSelectionResponse(true, Array.Empty<string>());
|
|
||||||
}
|
|
||||||
|
|
||||||
return await result.Content.ReadFromJsonAsync<FilesSelectionResponse>(this.jsonRustSerializerOptions);
|
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>
|
||||||
@ -67,6 +103,10 @@ public sealed partial class RustService
|
|||||||
/// <returns>A <see cref="FileSaveResponse"/> object containing information about whether the user canceled the
|
/// <returns>A <see cref="FileSaveResponse"/> object containing information about whether the user canceled the
|
||||||
/// 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)
|
||||||
|
{
|
||||||
|
return await this.RunFileDialog(
|
||||||
|
"save file",
|
||||||
|
async () =>
|
||||||
{
|
{
|
||||||
var payload = new SaveFileOptions
|
var payload = new SaveFileOptions
|
||||||
{
|
{
|
||||||
@ -76,13 +116,13 @@ public sealed partial class RustService
|
|||||||
};
|
};
|
||||||
|
|
||||||
var result = await this.http.PostAsJsonAsync("/save/file", payload, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/save/file", payload, this.jsonRustSerializerOptions);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (result.IsSuccessStatusCode)
|
||||||
{
|
|
||||||
this.logger!.LogError($"Failed to select a file for writing operation '{result.StatusCode}'");
|
|
||||||
return new FileSaveResponse(true, string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,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 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.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user