Added possibility that the user selects a file

This commit is contained in:
Thorsten Sommer 2025-01-08 20:50:48 +01:00
parent 0c5417ad31
commit cb744064d5
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
7 changed files with 158 additions and 0 deletions

View File

@ -0,0 +1,17 @@
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
<MudTextField
T="string"
Text="@this.File"
Label="@this.Label"
ReadOnly="@true"
Validation="@this.Validation"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.AttachFile"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="this.Disabled" OnClick="@this.OpenFileDialog">
Choose File
</MudButton>
</MudStack>

View File

@ -0,0 +1,63 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class SelectFile : ComponentBase
{
[Parameter]
public string File { get; set; } = string.Empty;
[Parameter]
public EventCallback<string> FileChanged { get; set; }
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string FileDialogTitle { get; set; } = "Select File";
[Parameter]
public Func<string, string?> Validation { get; set; } = _ => null;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
public RustService RustService { get; set; } = null!;
[Inject]
protected ILogger<SelectDirectory> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
await base.OnInitializedAsync();
}
#endregion
private void InternalFileChanged(string file)
{
this.File = file;
this.FileChanged.InvokeAsync(file);
}
private async Task OpenFileDialog()
{
var response = await this.RustService.SelectFile(this.FileDialogTitle, string.IsNullOrWhiteSpace(this.File) ? null : this.File);
this.Logger.LogInformation($"The user selected the file '{response.SelectedFilePath}'.");
if (!response.UserCancelled)
this.InternalFileChanged(response.SelectedFilePath);
}
}

View File

@ -0,0 +1,8 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for selecting a file.
/// </summary>
/// <param name="UserCancelled">Was the file selection canceled?</param>
/// <param name="SelectedFilePath">The selected file, if any.</param>
public readonly record struct FileSelectionResponse(bool UserCancelled, string SelectedFilePath);

View File

@ -0,0 +1,7 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for selecting a file when a previous file was selected.
/// </summary>
/// <param name="FilePath">The path of the previous file.</param>
public readonly record struct PreviousFile(string FilePath);

View File

@ -16,4 +16,17 @@ public sealed partial class RustService
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions); return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions);
} }
public async Task<FileSelectionResponse> SelectFile(string title, string? initialFile = null)
{
PreviousFile? previousFile = initialFile is null ? null : new (initialFile);
var result = await this.http.PostAsJsonAsync($"/select/file?title={title}", previousFile, this.jsonRustSerializerOptions);
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);
}
} }

View File

@ -270,4 +270,53 @@ pub struct PreviousDirectory {
pub struct DirectorySelectionResponse { pub struct DirectorySelectionResponse {
user_cancelled: bool, user_cancelled: bool,
selected_directory: String, selected_directory: String,
}
/// Let the user select a file.
#[post("/select/file?<title>", data = "<previous_file>")]
pub fn select_file(_token: APIToken, title: &str, previous_file: Option<Json<PreviousFile>>) -> Json<FileSelectionResponse> {
let file_path = match previous_file {
Some(previous) => {
let previous_path = previous.file_path.as_str();
FileDialogBuilder::new()
.set_title(title)
.set_directory(previous_path)
.pick_file()
},
None => {
FileDialogBuilder::new()
.set_title(title)
.pick_file()
},
};
match file_path {
Some(path) => {
info!("User selected file: {path:?}");
Json(FileSelectionResponse {
user_cancelled: false,
selected_file_path: path.to_str().unwrap().to_string(),
})
},
None => {
info!("User cancelled file selection.");
Json(FileSelectionResponse {
user_cancelled: true,
selected_file_path: String::from(""),
})
},
}
}
#[derive(Clone, Deserialize)]
pub struct PreviousFile {
file_path: String,
}
#[derive(Serialize)]
pub struct FileSelectionResponse {
user_cancelled: bool,
selected_file_path: String,
} }

View File

@ -85,6 +85,7 @@ pub fn start_runtime_api() {
crate::app_window::check_for_update, crate::app_window::check_for_update,
crate::app_window::install_update, crate::app_window::install_update,
crate::app_window::select_directory, crate::app_window::select_directory,
crate::app_window::select_file,
crate::secret::get_secret, crate::secret::get_secret,
crate::secret::store_secret, crate::secret::store_secret,
crate::secret::delete_secret, crate::secret::delete_secret,