using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; // This component has a parameter called File, which would shadow the file system's File class: using IOFile = System.IO.File; namespace AIStudio.Components; public partial class SelectFile : MSGComponentBase { [Parameter] public string File { get; set; } = string.Empty; [Parameter] public EventCallback 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 FileTypeFilter[]? Filter { get; set; } [Parameter] public Func Validation { get; set; } = _ => null; /// /// When true, the file can also be chosen by dropping it onto this component. /// [Parameter] public bool EnableDragDrop { get; set; } /// /// On which layer to register the drop area. Higher layers have priority over lower layers. /// [Parameter] public int Layer { get; set; } = DropLayers.ROOT; /// /// Catch all documents that are hovered over the AI Studio window and not only over the drop zone. /// [Parameter] public bool CatchAllDocuments { get; set; } [Inject] public RustService RustService { get; set; } = null!; [Inject] protected ILogger Logger { get; init; } = null!; private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); private bool isFileDialogOpen; #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() { 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 '{SelectedFilePath}'.", response.SelectedFilePath); if (!response.UserCancelled) this.InternalFileChanged(response.SelectedFilePath); } finally { this.isFileDialogOpen = false; } } /// /// Takes the first dropped path which leads to a usable file. /// /// /// This component carries exactly one file, so a multi-selection cannot be honored as a whole. /// A dropped folder is rejected instead of being read as "the first file inside it": the user /// was asked for a file, and picking one for them would be a surprise. /// /// The dropped paths. private async Task PathsDropped(List paths) { foreach (var path in paths) { if (!IOFile.Exists(path)) continue; if (this.Filter is { Length: > 0 } && !FileTypes.IsAllowedPath(path, this.Filter)) continue; this.Logger.LogInformation("The user dropped the file '{DroppedFilePath}'.", path); this.InternalFileChanged(path); return; } this.Logger.LogWarning("None of the {Count} dropped path(s) could be used as a file.", paths.Count); await this.MessageBus.SendWarning(new(Icons.Material.Filled.Warning, this.GetDropWarning(paths))); } private string GetDropWarning(List paths) { // Naming the actual mistake beats a generic "that did not work". Dropping a folder onto a // file picker is the likeliest of them: if (paths.Any(Directory.Exists)) return T("Please drop a file, not a folder."); // The file exists, so the file type filter is what turned it down: if (paths.Any(IOFile.Exists)) return T("Please drop a file with a supported file type."); return T("The dropped file could not be accessed. Please choose it with the file chooser instead."); } }