Added file type filtering to file selection dialogs (#442)

This commit is contained in:
Thorsten Sommer 2025-05-03 15:43:12 +02:00 committed by GitHub
parent 437ea2c243
commit 10dc03f33b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 83 additions and 22 deletions

View File

@ -1,3 +1,4 @@
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -17,12 +18,9 @@ public partial class ReadPDFContent : MSGComponentBase
private async Task SelectFile() private async Task SelectFile()
{ {
var pdfFile = await this.RustService.SelectFile(T("Select PDF file")); var pdfFile = await this.RustService.SelectFile(T("Select PDF file"), FileTypeFilter.PDF);
if (pdfFile.UserCancelled) if (pdfFile.UserCancelled)
return; return;
if (!pdfFile.SelectedFilePath.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
return;
if(!File.Exists(pdfFile.SelectedFilePath)) if(!File.Exists(pdfFile.SelectedFilePath))
return; return;

View File

@ -1,4 +1,5 @@
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -22,6 +23,9 @@ public partial class SelectFile : ComponentBase
[Parameter] [Parameter]
public string FileDialogTitle { get; set; } = "Select File"; public string FileDialogTitle { get; set; } = "Select File";
[Parameter]
public FileTypeFilter? Filter { get; set; }
[Parameter] [Parameter]
public Func<string, string?> Validation { get; set; } = _ => null; public Func<string, string?> Validation { get; set; } = _ => null;
@ -55,7 +59,7 @@ public partial class SelectFile : ComponentBase
private async Task OpenFileDialog() private async Task OpenFileDialog()
{ {
var response = await this.RustService.SelectFile(this.FileDialogTitle, 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 '{response.SelectedFilePath}'.");
if (!response.UserCancelled) if (!response.UserCancelled)

View File

@ -0,0 +1,18 @@
// ReSharper disable NotAccessedPositionalProperty.Global
namespace AIStudio.Tools.Rust;
/// <summary>
/// Represents a file type filter for file selection dialogs.
/// </summary>
/// <param name="FilterName">The name of the filter.</param>
/// <param name="FilterExtensions">The file extensions associated with the filter.</param>
public readonly record struct FileTypeFilter(string FilterName, string[] FilterExtensions)
{
public static FileTypeFilter PDF => new("PDF Files", ["pdf"]);
public static FileTypeFilter Text => new("Text Files", ["txt", "md"]);
public static FileTypeFilter AllOffice => new("All Office Files", ["docx", "xlsx", "pptx", "doc", "xls", "ppt", "pdf"]);
public static FileTypeFilter AllImages => new("All Image Files", ["jpg", "jpeg", "png", "gif", "bmp", "tiff"]);
}

View File

@ -0,0 +1,10 @@
namespace AIStudio.Tools.Rust;
public sealed class SelectFileOptions
{
public required string Title { get; init; }
public PreviousFile? PreviousFile { get; init; }
public FileTypeFilter? Filter { get; init; }
}

View File

@ -17,10 +17,16 @@ 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) public async Task<FileSelectionResponse> SelectFile(string title, FileTypeFilter? filter = null, string? initialFile = null)
{ {
PreviousFile? previousFile = initialFile is null ? null : new (initialFile); var payload = new SelectFileOptions
var result = await this.http.PostAsJsonAsync($"/select/file?title={title}", previousFile, this.jsonRustSerializerOptions); {
Title = title,
PreviousFile = initialFile is null ? null : new (initialFile),
Filter = filter
};
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}'"); this.logger!.LogError($"Failed to select a file: '{result.StatusCode}'");

View File

@ -267,6 +267,19 @@ pub struct PreviousDirectory {
path: String, path: String,
} }
#[derive(Clone, Deserialize)]
pub struct FileTypeFilter {
filter_name: String,
filter_extensions: Vec<String>,
}
#[derive(Clone, Deserialize)]
pub struct SelectFileOptions {
title: String,
previous_file: Option<PreviousFile>,
filter: Option<FileTypeFilter>,
}
#[derive(Serialize)] #[derive(Serialize)]
pub struct DirectorySelectionResponse { pub struct DirectorySelectionResponse {
user_cancelled: bool, user_cancelled: bool,
@ -274,24 +287,36 @@ pub struct DirectorySelectionResponse {
} }
/// Let the user select a file. /// Let the user select a file.
#[post("/select/file?<title>", data = "<previous_file>")] #[post("/select/file", data = "<payload>")]
pub fn select_file(_token: APIToken, title: &str, previous_file: Option<Json<PreviousFile>>) -> Json<FileSelectionResponse> { pub fn select_file(_token: APIToken, payload: Json<SelectFileOptions>) -> Json<FileSelectionResponse> {
let file_path = match previous_file {
Some(previous) => { // Create a new file dialog builder:
let previous_path = previous.file_path.as_str(); let file_dialog = FileDialogBuilder::new();
FileDialogBuilder::new()
.set_title(title) // Set the title of the file dialog:
.set_directory(previous_path) let file_dialog = file_dialog.set_title(&payload.title);
.pick_file()
// Set the file type filter if provided:
let file_dialog = match &payload.filter {
Some(filter) => {
file_dialog.add_filter(&filter.filter_name, &filter.filter_extensions.iter().map(|s| s.as_str()).collect::<Vec<&str>>())
}, },
None => { None => file_dialog,
FileDialogBuilder::new()
.set_title(title)
.pick_file()
},
}; };
// Set the previous file path if provided:
let file_dialog = match &payload.previous_file {
Some(previous) => {
let previous_path = previous.file_path.as_str();
file_dialog.set_directory(previous_path)
},
None => file_dialog,
};
// Show the file dialog and get the selected file path:
let file_path = file_dialog.pick_file();
match file_path { match file_path {
Some(path) => { Some(path) => {
info!("User selected file: {path:?}"); info!("User selected file: {path:?}");