diff --git a/LANCommander.Server.Services/Models/MediaGrabberDownloadResult.cs b/LANCommander.Server.Services/Models/MediaGrabberDownloadResult.cs index 5f7c0e18..023d13ad 100644 --- a/LANCommander.Server.Services/Models/MediaGrabberDownloadResult.cs +++ b/LANCommander.Server.Services/Models/MediaGrabberDownloadResult.cs @@ -1,6 +1,15 @@ +using System.Collections.Generic; + namespace LANCommander.Server.Services.Models { public class MediaGrabberDownloadResult + { + public MediaGrabberResult Result { get; set; } + public MediaGrabberDownload Download { get; set; } + public List Results { get; set; } = new(); + } + + public class MediaGrabberDownloadResultEntry { public MediaGrabberResult Result { get; set; } public MediaGrabberDownload Download { get; set; } diff --git a/LANCommander.Server.Services/Models/MediaGrabberOptions.cs b/LANCommander.Server.Services/Models/MediaGrabberOptions.cs index 07f60acc..6f67a51d 100644 --- a/LANCommander.Server.Services/Models/MediaGrabberOptions.cs +++ b/LANCommander.Server.Services/Models/MediaGrabberOptions.cs @@ -6,5 +6,6 @@ namespace LANCommander.Server.Services.Models { public MediaType Type { get; set; } public string Search { get; set; } + public bool MultiSelect { get; set; } } } diff --git a/LANCommander.Server/UI/Components/ImagePicker.razor b/LANCommander.Server/UI/Components/ImagePicker.razor index a080a757..b623bedf 100644 --- a/LANCommander.Server/UI/Components/ImagePicker.razor +++ b/LANCommander.Server/UI/Components/ImagePicker.razor @@ -5,7 +5,14 @@ var id = Guid.NewGuid();
- + @if (MultiSelect) + { + + } + else + { + + }
@@ -19,6 +26,9 @@ [Parameter] public EventCallback ValueChanged { get; set; } [Parameter] public EventCallback OnPicked { get; set; } [Parameter] public Dictionary Images { get; set; } + [Parameter] public bool MultiSelect { get; set; } + [Parameter] public HashSet SelectedValues { get; set; } = new(); + [Parameter] public EventCallback> SelectedValuesChanged { get; set; } async Task SelectionChanged(string key) { @@ -28,6 +38,15 @@ await ValueChanged.InvokeAsync(key); } + async Task ToggleSelection(string key) + { + if (!SelectedValues.Add(key)) + SelectedValues.Remove(key); + + if (SelectedValuesChanged.HasDelegate) + await SelectedValuesChanged.InvokeAsync(SelectedValues); + } + async Task Pick(string key) { Value = key; diff --git a/LANCommander.Server/UI/Components/MediaGrabberDialog.razor b/LANCommander.Server/UI/Components/MediaGrabberDialog.razor index fb7602e4..d5a916fd 100644 --- a/LANCommander.Server/UI/Components/MediaGrabberDialog.razor +++ b/LANCommander.Server/UI/Components/MediaGrabberDialog.razor @@ -73,7 +73,7 @@ } else { - + } } @@ -108,6 +108,9 @@ MediaGrabberResult Media { get; set; } MediaGrabberResult _preview { get; set; } + bool MultiSelect { get; set; } + HashSet _selectedKeys = new(); + double Size { get; set; } = 200; bool Loading { get; set; } = true; bool _isDownloading; @@ -122,6 +125,7 @@ { Type = Options.Type; Search = Options.Search; + MultiSelect = Options.MultiSelect; await GetResults(Type, Search); } @@ -138,6 +142,7 @@ _preview = null; _allResults.Clear(); _groupedResults.Clear(); + _selectedKeys.Clear(); await InvokeAsync(StateHasChanged); if (String.IsNullOrWhiteSpace(search)) @@ -198,10 +203,25 @@ Media = _allResults.FirstOrDefault(r => r.UniqueKey == uniqueKey); } + private void OnMultiImageSelectionChanged(HashSet selectedKeys) + { + _selectedKeys = selectedKeys; + } + private async Task OnImagePicked(string uniqueKey) { - OnImageSelected(uniqueKey); - await DownloadAndClose(); + if (MultiSelect) + { + if (!_selectedKeys.Add(uniqueKey)) + _selectedKeys.Remove(uniqueKey); + + await InvokeAsync(StateHasChanged); + } + else + { + OnImageSelected(uniqueKey); + await DownloadAndClose(); + } } private void PreviewVideo(MediaGrabberResult result) @@ -234,6 +254,12 @@ private async Task DownloadAndClose() { + if (MultiSelect) + { + await DownloadMultipleAndClose(); + return; + } + if (Media == null) return; @@ -272,6 +298,56 @@ } } + private async Task DownloadMultipleAndClose() + { + var selectedResults = _allResults.Where(r => _selectedKeys.Contains(r.UniqueKey)).ToList(); + + if (!selectedResults.Any()) + return; + + _preview = null; + _isDownloading = true; + _downloadPercent = 0; + await InvokeAsync(StateHasChanged); + + try + { + var downloadResult = new MediaGrabberDownloadResult(); + + for (int i = 0; i < selectedResults.Count; i++) + { + var selected = selectedResults[i]; + _downloadStatus = $"Downloading {i + 1} of {selectedResults.Count}..."; + _downloadPercent = 0; + await InvokeAsync(StateHasChanged); + + var progress = new Progress(p => + { + _downloadPercent = p.Percent ?? 0; + InvokeAsync(StateHasChanged); + }); + + var download = await MediaGrabberService.DownloadAsync(selected, progress); + + downloadResult.Results.Add(new MediaGrabberDownloadResultEntry + { + Result = selected, + Download = download + }); + } + + await base.OkCancelRefWithResult!.OnOk(downloadResult); + await CloseFeedbackAsync(); + } + catch (Exception ex) + { + _isDownloading = false; + await InvokeAsync(StateHasChanged); + MessageService.Error("Download failed. Please try again."); + Logger.LogError(ex, "Failed to download media"); + } + } + private class GroupedResults { public string GrabberName { get; set; } diff --git a/LANCommander.Server/UI/Pages/Games/Components/GameDetailPreview.razor b/LANCommander.Server/UI/Pages/Games/Components/GameDetailPreview.razor index 5fbf16d2..79b8c340 100644 --- a/LANCommander.Server/UI/Pages/Games/Components/GameDetailPreview.razor +++ b/LANCommander.Server/UI/Pages/Games/Components/GameDetailPreview.razor @@ -425,6 +425,8 @@ async Task SearchMediaByType(MediaType type) { + var multiSelect = type == MediaType.Screenshot; + var modalOptions = new ModalOptions { Title = $"Download {type}", @@ -437,43 +439,68 @@ var grabberOptions = new MediaGrabberOptions { Type = type, - Search = _game.Title + Search = _game.Title, + MultiSelect = multiSelect }; var modalRef = await ModalService.CreateModalAsync(modalOptions, grabberOptions); modalRef.OnOk = async (downloadResult) => { - using var download = downloadResult.Download; + if (multiSelect && downloadResult.Results.Any()) + { + foreach (var entry in downloadResult.Results) + { + using var download = entry.Download; + + var media = new Media + { + GameId = GameId, + Type = type, + SourceUrl = entry.Result.SourceUrl, + MimeType = download.MimeType, + StorageLocation = await MediaService.GetDefaultStorageLocationAsync(), + Crc32 = string.Empty, + SortOrder = GetNextSortOrder(type) + }; + + await MediaService.WriteToFileAsync(media, download.Stream); + } + + await RefreshMedia(); + return; + } + + using var singleDownload = downloadResult.Download; var isSingular = type != MediaType.Screenshot && type != MediaType.Video; var existing = isSingular ? GetMedia(type) : null; - Media media; + Media singleMedia; if (existing != null) { - media = existing; - media.SourceUrl = downloadResult.Result.SourceUrl; - media.MimeType = download.MimeType; + singleMedia = existing; + singleMedia.SourceUrl = downloadResult.Result.SourceUrl; + singleMedia.MimeType = singleDownload.MimeType; - MediaService.DeleteLocalMediaFile(media); - media = await MediaService.WriteToFileAsync(media, download.Stream); + MediaService.DeleteLocalMediaFile(singleMedia); + singleMedia = await MediaService.WriteToFileAsync(singleMedia, singleDownload.Stream); } else { - media = new Media + singleMedia = new Media { GameId = GameId, Type = type, SourceUrl = downloadResult.Result.SourceUrl, - MimeType = download.MimeType, + MimeType = singleDownload.MimeType, StorageLocation = await MediaService.GetDefaultStorageLocationAsync(), Crc32 = string.Empty, SortOrder = GetNextSortOrder(type) }; - media = await MediaService.WriteToFileAsync(media, download.Stream); + singleMedia = await MediaService.WriteToFileAsync(singleMedia, singleDownload.Stream); } await RefreshMedia();