From 2f842f8cdc1494136fbce881c261a59523110a65 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Wed, 6 May 2026 15:31:18 -0500 Subject: [PATCH] Introduce IFilePickerService and move SettingsModal and filter modals to EventLogExpert.Components --- .../Modals/Filters/FilterCacheModal.razor | 0 .../Modals/Filters/FilterCacheModal.razor.cs | 22 +++--- .../Modals/Filters/FilterCacheModal.razor.css | 0 .../Modals/Filters/FilterGroup.razor | 0 .../Modals/Filters/FilterGroup.razor.cs | 19 +++-- .../Modals/Filters/FilterGroupModal.razor | 0 .../Modals/Filters/FilterGroupModal.razor.cs | 19 +++-- .../Modals/Filters/FilterGroupSection.razor | 0 .../Filters/FilterGroupSection.razor.cs | 0 .../Modals/SettingsModal.razor | 0 .../Modals/SettingsModal.razor.cs | 23 ++---- .../Modals/SettingsModal.razor.css | 0 .../Interfaces/IFilePickerService.cs | 25 +++++++ src/EventLogExpert/MauiProgram.cs | 1 + .../Services/MauiFilePickerService.cs | 72 +++++++++++++++++++ 15 files changed, 128 insertions(+), 53 deletions(-) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/Filters/FilterCacheModal.razor (100%) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/Filters/FilterCacheModal.razor.cs (84%) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/Filters/FilterCacheModal.razor.css (100%) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/Filters/FilterGroup.razor (100%) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/Filters/FilterGroup.razor.cs (92%) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/Filters/FilterGroupModal.razor (100%) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/Filters/FilterGroupModal.razor.cs (81%) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/Filters/FilterGroupSection.razor (100%) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/Filters/FilterGroupSection.razor.cs (100%) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/SettingsModal.razor (100%) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/SettingsModal.razor.cs (95%) rename src/{EventLogExpert/Components => EventLogExpert.Components}/Modals/SettingsModal.razor.css (100%) create mode 100644 src/EventLogExpert.UI/Interfaces/IFilePickerService.cs create mode 100644 src/EventLogExpert/Services/MauiFilePickerService.cs diff --git a/src/EventLogExpert/Components/Modals/Filters/FilterCacheModal.razor b/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor similarity index 100% rename from src/EventLogExpert/Components/Modals/Filters/FilterCacheModal.razor rename to src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor diff --git a/src/EventLogExpert/Components/Modals/Filters/FilterCacheModal.razor.cs b/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs similarity index 84% rename from src/EventLogExpert/Components/Modals/Filters/FilterCacheModal.razor.cs rename to src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs index 1cd1bdbe..7fe82e5e 100644 --- a/src/EventLogExpert/Components/Modals/Filters/FilterCacheModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs @@ -20,6 +20,8 @@ public sealed partial class FilterCacheModal : ModalBase [Inject] private IDispatcher Dispatcher { get; init; } = null!; + [Inject] private IFilePickerService FilePickerService { get; init; } = null!; + [Inject] private IFileSaveService FileSaveService { get; init; } = null!; [Inject] private IState FilterCacheState { get; init; } = null!; @@ -45,23 +47,15 @@ await AlertDialogService.ShowAlert("Export Failed", protected override async Task OnImportAsync() { - PickOptions options = new() + try { - PickerTitle = "Please select a json file to import", - FileTypes = new FilePickerFileType( - new Dictionary> - { - { DevicePlatform.WinUI, [".json"] } - }) - }; - - var result = await FilePicker.Default.PickAsync(options); + var path = await FilePickerService.PickAsync( + "Please select a json file to import", + FilePickerServiceFileTypes.Json); - if (result is null) { return; } + if (path is null) { return; } - try - { - await using var stream = File.OpenRead(result.FullPath); + await using var stream = File.OpenRead(path); var filters = await JsonSerializer.DeserializeAsync>(stream); if (filters is null) { return; } diff --git a/src/EventLogExpert/Components/Modals/Filters/FilterCacheModal.razor.css b/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.css similarity index 100% rename from src/EventLogExpert/Components/Modals/Filters/FilterCacheModal.razor.css rename to src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.css diff --git a/src/EventLogExpert/Components/Modals/Filters/FilterGroup.razor b/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor similarity index 100% rename from src/EventLogExpert/Components/Modals/Filters/FilterGroup.razor rename to src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor diff --git a/src/EventLogExpert/Components/Modals/Filters/FilterGroup.razor.cs b/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs similarity index 92% rename from src/EventLogExpert/Components/Modals/Filters/FilterGroup.razor.cs rename to src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs index ffbe2dfb..515d87ea 100644 --- a/src/EventLogExpert/Components/Modals/Filters/FilterGroup.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs @@ -29,6 +29,8 @@ public sealed partial class FilterGroup [Inject] private IDispatcher Dispatcher { get; init; } = null!; + [Inject] private IFilePickerService FilePickerService { get; init; } = null!; + [Inject] private IFileSaveService FileSaveService { get; init; } = null!; protected override void OnParametersSet() @@ -110,20 +112,15 @@ private void HandlePendingSave(FilterDraftModel draft, FilterModel filter) private async Task ImportGroup() { - PickOptions options = new() + try { - PickerTitle = "Please select a json file to import", - FileTypes = new FilePickerFileType( - new Dictionary> { { DevicePlatform.WinUI, [".json"] } }) - }; - - var result = await FilePicker.Default.PickAsync(options); + var path = await FilePickerService.PickAsync( + "Please select a json file to import", + FilePickerServiceFileTypes.Json); - if (result is null) { return; } + if (path is null) { return; } - try - { - await using var stream = File.OpenRead(result.FullPath); + await using var stream = File.OpenRead(path); var group = await JsonSerializer.DeserializeAsync(stream); if (group is null) { return; } diff --git a/src/EventLogExpert/Components/Modals/Filters/FilterGroupModal.razor b/src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor similarity index 100% rename from src/EventLogExpert/Components/Modals/Filters/FilterGroupModal.razor rename to src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor diff --git a/src/EventLogExpert/Components/Modals/Filters/FilterGroupModal.razor.cs b/src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor.cs similarity index 81% rename from src/EventLogExpert/Components/Modals/Filters/FilterGroupModal.razor.cs rename to src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor.cs index 90a1e94e..b7a7bcb2 100644 --- a/src/EventLogExpert/Components/Modals/Filters/FilterGroupModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor.cs @@ -18,6 +18,8 @@ public sealed partial class FilterGroupModal : ModalBase [Inject] private IDispatcher Dispatcher { get; init; } = null!; + [Inject] private IFilePickerService FilePickerService { get; init; } = null!; + [Inject] private IFileSaveService FileSaveService { get; init; } = null!; [Inject] private IState FilterGroupState { get; init; } = null!; @@ -43,20 +45,15 @@ await AlertDialogService.ShowAlert("Export Failed", protected override async Task OnImportAsync() { - PickOptions options = new() + try { - PickerTitle = "Please select a json file to import", - FileTypes = new FilePickerFileType( - new Dictionary> { { DevicePlatform.WinUI, [".json"] } }) - }; - - var result = await FilePicker.Default.PickAsync(options); + var path = await FilePickerService.PickAsync( + "Please select a json file to import", + FilePickerServiceFileTypes.Json); - if (result is null) { return; } + if (path is null) { return; } - try - { - await using var stream = File.OpenRead(result.FullPath); + await using var stream = File.OpenRead(path); var groups = await JsonSerializer.DeserializeAsync>(stream); if (groups is null) { return; } diff --git a/src/EventLogExpert/Components/Modals/Filters/FilterGroupSection.razor b/src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor similarity index 100% rename from src/EventLogExpert/Components/Modals/Filters/FilterGroupSection.razor rename to src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor diff --git a/src/EventLogExpert/Components/Modals/Filters/FilterGroupSection.razor.cs b/src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor.cs similarity index 100% rename from src/EventLogExpert/Components/Modals/Filters/FilterGroupSection.razor.cs rename to src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor.cs diff --git a/src/EventLogExpert/Components/Modals/SettingsModal.razor b/src/EventLogExpert.Components/Modals/SettingsModal.razor similarity index 100% rename from src/EventLogExpert/Components/Modals/SettingsModal.razor rename to src/EventLogExpert.Components/Modals/SettingsModal.razor diff --git a/src/EventLogExpert/Components/Modals/SettingsModal.razor.cs b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs similarity index 95% rename from src/EventLogExpert/Components/Modals/SettingsModal.razor.cs rename to src/EventLogExpert.Components/Modals/SettingsModal.razor.cs index 7297ea79..a5e0c9da 100644 --- a/src/EventLogExpert/Components/Modals/SettingsModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs @@ -39,6 +39,8 @@ public sealed partial class SettingsModal : ModalBase [Inject] private IState EventLogState { get; init; } = null!; + [Inject] private IFilePickerService FilePickerService { get; init; } = null!; + [Inject] private ILogReloadCoordinator LogReloadCoordinator { get; init; } = null!; private bool IsClassificationPending => !DatabaseService.InitialClassificationTask.IsCompleted; @@ -192,26 +194,13 @@ private bool GetEffectiveEnabled(DatabaseEntry entry) => private async Task ImportDatabase() { - PickOptions options = new() - { - PickerTitle = "Please select a database file", - FileTypes = new FilePickerFileType( - new Dictionary> - { - { DevicePlatform.WinUI, [".db", ".zip"] } - }) - }; - try { - var result = (await FilePicker.Default.PickMultipleAsync(options)).ToArray(); - - if (result.Length <= 0) { return; } + var sourcePaths = await FilePickerService.PickMultipleAsync( + "Please select database files to import", + FilePickerServiceFileTypes.Database); - var sourcePaths = result - .Where(item => item is not null && !string.IsNullOrEmpty(item.FullPath)) - .Select(item => item!.FullPath) - .ToList(); + if (sourcePaths.Count == 0) { return; } var skipFileNames = await ResolveImportConflictsAsync(sourcePaths, CancellationToken.None); diff --git a/src/EventLogExpert/Components/Modals/SettingsModal.razor.css b/src/EventLogExpert.Components/Modals/SettingsModal.razor.css similarity index 100% rename from src/EventLogExpert/Components/Modals/SettingsModal.razor.css rename to src/EventLogExpert.Components/Modals/SettingsModal.razor.css diff --git a/src/EventLogExpert.UI/Interfaces/IFilePickerService.cs b/src/EventLogExpert.UI/Interfaces/IFilePickerService.cs new file mode 100644 index 00000000..24335b20 --- /dev/null +++ b/src/EventLogExpert.UI/Interfaces/IFilePickerService.cs @@ -0,0 +1,25 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Interfaces; + +public interface IFilePickerService +{ + /// + /// Opens a system "Open File" dialog filtered to ; returns the picked path or + /// null if the user cancelled. + /// + Task PickAsync(string pickerTitle, IReadOnlyList extensions); + + /// + /// Opens a multi-select "Open File" dialog filtered to ; returns the picked + /// paths (empty if the user cancelled). + /// + Task> PickMultipleAsync(string pickerTitle, IReadOnlyList extensions); +} + +public static class FilePickerServiceFileTypes +{ + public static readonly IReadOnlyList Database = [".db", ".zip"]; + public static readonly IReadOnlyList Json = [".json"]; +} diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 5e3e9064..71c5ecf4 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -98,6 +98,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/EventLogExpert/Services/MauiFilePickerService.cs b/src/EventLogExpert/Services/MauiFilePickerService.cs new file mode 100644 index 00000000..ca8e21e5 --- /dev/null +++ b/src/EventLogExpert/Services/MauiFilePickerService.cs @@ -0,0 +1,72 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Interfaces; + +namespace EventLogExpert.Services; + +public sealed class MauiFilePickerService : IFilePickerService +{ + public Task PickAsync(string pickerTitle, IReadOnlyList extensions) + { + ArgumentNullException.ThrowIfNull(pickerTitle); + ArgumentNullException.ThrowIfNull(extensions); + + if (extensions.Count == 0) + { + throw new ArgumentException( + "At least one extension must be supplied.", nameof(extensions)); + } + + return MainThread.InvokeOnMainThreadAsync(async () => + { + var options = BuildOptions(pickerTitle, extensions); + var result = await FilePicker.Default.PickAsync(options); + return string.IsNullOrEmpty(result?.FullPath) ? null : result.FullPath; + }); + } + + public Task> PickMultipleAsync( + string pickerTitle, + IReadOnlyList extensions) + { + ArgumentNullException.ThrowIfNull(pickerTitle); + ArgumentNullException.ThrowIfNull(extensions); + + if (extensions.Count == 0) + { + throw new ArgumentException( + "At least one extension must be supplied.", nameof(extensions)); + } + + return MainThread.InvokeOnMainThreadAsync>(async () => + { + var options = BuildOptions(pickerTitle, extensions); + var results = await FilePicker.Default.PickMultipleAsync(options) ?? []; + var paths = new List(); + + foreach (var result in results) + { + var path = result?.FullPath; + + if (!string.IsNullOrEmpty(path)) + { + paths.Add(path); + } + } + + return paths; + }); + } + + private static PickOptions BuildOptions(string pickerTitle, IReadOnlyList extensions) => + new() + { + PickerTitle = pickerTitle, + FileTypes = new FilePickerFileType( + new Dictionary> + { + { DevicePlatform.WinUI, extensions } + }) + }; +}