Add multiple select support for media grabber

This commit is contained in:
Pat Hartl 2026-04-30 02:16:17 -05:00
parent 232f86ba85
commit f0b85de93f
5 changed files with 147 additions and 15 deletions

View file

@ -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<MediaGrabberDownloadResultEntry> Results { get; set; } = new();
}
public class MediaGrabberDownloadResultEntry
{
public MediaGrabberResult Result { get; set; }
public MediaGrabberDownload Download { get; set; }

View file

@ -6,5 +6,6 @@ namespace LANCommander.Server.Services.Models
{
public MediaType Type { get; set; }
public string Search { get; set; }
public bool MultiSelect { get; set; }
}
}

View file

@ -5,7 +5,14 @@
var id = Guid.NewGuid();
<div class="media-picker-item" style="width: @(Size)px; max-height: @(Size)px">
<input type="radio" id="media-picker-item-@id" checked="@(Value == image.Key)" name="SelectedResult" @onchange="@(() => SelectionChanged(image.Key))" />
@if (MultiSelect)
{
<input type="checkbox" id="media-picker-item-@id" checked="@(SelectedValues.Contains(image.Key))" name="SelectedResult" @onchange="@(() => ToggleSelection(image.Key))" />
}
else
{
<input type="radio" id="media-picker-item-@id" checked="@(Value == image.Key)" name="SelectedResult" @onchange="@(() => SelectionChanged(image.Key))" />
}
<label for="media-picker-item-@id" @ondblclick="@(() => Pick(image.Key))"></label>
<img src="@image.Value" />
</div>
@ -19,6 +26,9 @@
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public EventCallback<string> OnPicked { get; set; }
[Parameter] public Dictionary<string, string> Images { get; set; }
[Parameter] public bool MultiSelect { get; set; }
[Parameter] public HashSet<string> SelectedValues { get; set; } = new();
[Parameter] public EventCallback<HashSet<string>> 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;

View file

@ -73,7 +73,7 @@
}
else
{
<ImagePicker Size="Size" Images="@grabberGroup.Results.ToDictionary(r => r.UniqueKey, r => r.ThumbnailUrl)" ValueChanged="OnImageSelected" OnPicked="OnImagePicked" />
<ImagePicker Size="Size" MultiSelect="MultiSelect" SelectedValues="_selectedKeys" SelectedValuesChanged="OnMultiImageSelectionChanged" Images="@grabberGroup.Results.ToDictionary(r => r.UniqueKey, r => r.ThumbnailUrl)" ValueChanged="OnImageSelected" OnPicked="OnImagePicked" />
}
</div>
}
@ -108,6 +108,9 @@
MediaGrabberResult Media { get; set; }
MediaGrabberResult _preview { get; set; }
bool MultiSelect { get; set; }
HashSet<string> _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<string> 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<MediaDownloadProgress>(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; }

View file

@ -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<MediaGrabberDialog, MediaGrabberOptions, MediaGrabberDownloadResult>(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();