Skip to content

Commit 6e68d3a

Browse files
committed
Improve selection
1 parent c0f8de5 commit 6e68d3a

11 files changed

Lines changed: 77 additions & 87 deletions

File tree

Muxarr.Data/Entities/AuditableEntity.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
namespace Muxarr.Data.Entities;
55

6+
public interface IHasId
7+
{
8+
int Id { get; }
9+
}
10+
611
public abstract class AuditableEntity
712
{
813
public DateTime CreatedDate { get; set; }

Muxarr.Data/Entities/MediaConversion.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
namespace Muxarr.Data.Entities;
77

8-
public class MediaConversion : AuditableEntity
8+
public class MediaConversion : AuditableEntity, IHasId
99
{
1010
public int Id { get; set; }
1111
public int? MediaFileId { get; set; }

Muxarr.Data/Entities/MediaFile.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace Muxarr.Data.Entities;
55

6-
public class MediaFile : AuditableEntity
6+
public class MediaFile : AuditableEntity, IHasId
77
{
88
public int Id { get; set; }
99
public int ProfileId { get; set; }

Muxarr.Data/Extensions/PaginationExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
using System.Linq.Expressions;
22
using System.Reflection;
33
using Microsoft.EntityFrameworkCore;
4+
using Muxarr.Data.Entities;
45

56
namespace Muxarr.Data.Extensions;
67

78
public static class PagingExtensions
89
{
10+
// EF expands Contains to one parameter per id and SQLite caps a statement
11+
// at 32766, so large id sets load in chunks.
12+
public static async Task<List<T>> WhereIdsAsync<T>(this IQueryable<T> query, IReadOnlyCollection<int> ids)
13+
where T : class, IHasId
14+
{
15+
var result = new List<T>(ids.Count);
16+
foreach (var chunk in ids.Chunk(10_000))
17+
{
18+
result.AddRange(await query.Where(e => chunk.Contains(e.Id)).ToListAsync());
19+
}
20+
21+
return result;
22+
}
23+
924
public static async Task<(IQueryable<T> Data, int Total, int TotalPages)> FindPagedAsync<T>(
1025
this IQueryable<T> collection,
1126
int pageNumber,

Muxarr.Web/Components/Pages/Conversions/Index.razor

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,11 @@
118118
}
119119

120120
<div class="row mb-3 g-2 mux-filter-row">
121-
@if (SelectedItems.Count > 0)
121+
@if (SelectedIds.Count > 0)
122122
{
123123
<div class="col-auto">
124124
<button class="btn btn-danger btn-sm" @onclick="OnDeleteSelected">
125-
<i class="bi bi-trash me-1"></i>Delete @SelectedItems.Count selected
125+
<i class="bi bi-trash me-1"></i>Delete @SelectedIds.Count selected
126126
</button>
127127
</div>
128128
}
@@ -353,24 +353,28 @@
353353

354354
private async Task OnDeleteSelected()
355355
{
356-
var deletable = SelectedItems.Where(c => c.State != ConversionState.Processing).ToList();
357-
if (deletable.Count == 0)
356+
// Check the state at delete time; the selected rows may be stale.
357+
var ids = SelectedIds.ToList();
358+
await using var context = await ContextFactory.CreateDbContextAsync();
359+
var deletable = await context.MediaConversions
360+
.CountAsync(c => ids.Contains(c.Id) && c.State != ConversionState.Processing);
361+
if (deletable == 0)
358362
{
359363
return;
360364
}
361365

362366
if (!await ModalService.ShowConfirm(
363-
$"Are you sure you want to delete {deletable.Count} conversion(s)? This will affect conversion statistics.", "Delete Conversions"))
367+
$"Are you sure you want to delete {deletable} conversion(s)? This will affect conversion statistics.", "Delete Conversions"))
364368
{
365369
return;
366370
}
367371

368-
var ids = deletable.Select(c => c.Id).ToList();
369-
await using var context = await ContextFactory.CreateDbContextAsync();
370-
await context.MediaConversions.Where(c => ids.Contains(c.Id)).ExecuteDeleteAsync();
371-
SelectedItems.Clear();
372+
var deleted = await context.MediaConversions
373+
.Where(c => ids.Contains(c.Id) && c.State != ConversionState.Processing)
374+
.ExecuteDeleteAsync();
375+
SelectedIds.Clear();
372376
await UpdateList();
373-
ToastService.ShowSuccess($"Deleted {deletable.Count} conversion(s).");
377+
ToastService.ShowSuccess($"Deleted {deleted} conversion(s).");
374378
}
375379

376380
private void OnQueueStateChanged()

Muxarr.Web/Components/Pages/Library/BatchEdit.razor

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -346,15 +346,7 @@
346346

347347
await using (var context = await ContextFactory.CreateDbContextAsync())
348348
{
349-
// EF expands Contains to one SQL parameter per id and SQLite caps a
350-
// statement at 32766, so a whole-library selection arrives in batches.
351-
foreach (var chunk in ids.Chunk(10000))
352-
{
353-
_files.AddRange(await context.MediaFiles
354-
.Where(f => chunk.Contains(f.Id))
355-
.WithTracksAndProfile()
356-
.ToListAsync());
357-
}
349+
_files = await context.MediaFiles.WithTracksAndProfile().WhereIdsAsync(ids);
358350
}
359351

360352
_slots = Aggregate(_files);

Muxarr.Web/Components/Pages/Library/Index.razor

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
</div>
4444

4545
<div class="row mb-3 g-2 mux-filter-row">
46-
@if (SelectedItems.Count > 0)
46+
@if (SelectedIds.Count > 0)
4747
{
4848
<div class="col-auto">
4949
<button @onclick="QueueSelected" class="btn btn-success btn-sm" type="button"
@@ -56,13 +56,13 @@
5656
{
5757
<i class="bi bi-plus-circle me-1"></i>
5858
}
59-
Queue @SelectedItems.Count to conversion
59+
Queue @SelectedIds.Count to conversion
6060
</button>
6161
</div>
6262
<div class="col-auto">
6363
<button @onclick="BatchEditSelected" class="btn btn-outline-primary btn-sm" type="button"
6464
disabled="@_isQueueing">
65-
<i class="bi bi-sliders me-1"></i>Edit tracks on @SelectedItems.Count
65+
<i class="bi bi-sliders me-1"></i>Edit tracks on @SelectedIds.Count
6666
</button>
6767
</div>
6868
}
@@ -375,9 +375,6 @@
375375
private bool _isQueueing;
376376
private bool _disposed;
377377

378-
// Ticks handed back by the batch editor. Consumed by the first list load,
379-
// then dropped so paging and filtering don't keep re-ticking rows.
380-
private HashSet<int>? _restoreIds;
381378
private LibraryStatsConfig? _filterOptions;
382379
private List<ProfileSummary> _profileSummaries = [];
383380

@@ -416,7 +413,12 @@
416413
IsAscending = false;
417414

418415
MediaScanner.ScanningStateChanged += OnScanningStateChanged;
419-
_restoreIds = Selections.Get(SelectionToken);
416+
417+
// Ticks handed back by the batch editor; ids, so no page needs to be loaded.
418+
if (Selections.Get(SelectionToken) is { } restored)
419+
{
420+
SelectedIds.UnionWith(restored);
421+
}
420422

421423
await LoadFilters();
422424

@@ -425,9 +427,6 @@
425427
await UpdateList();
426428
}
427429

428-
// The restore above has run; from here the ticks are the user's business again.
429-
_restoreIds = null;
430-
431430
await using var context = await ContextFactory.CreateDbContextAsync();
432431
_filterOptions = context.Configs.Get<LibraryStatsConfig>();
433432
await LoadProfileSummaries(context);
@@ -481,15 +480,6 @@
481480
TotalItems = result.Total;
482481
TotalPages = result.TotalPages;
483482
Items = await result.Data.WithTracks().ToListAsync().ConfigureAwait(false);
484-
485-
// Ticks come back after a trip to the batch editor, which only ever holds ids.
486-
if (_restoreIds != null)
487-
{
488-
foreach (var file in Items.Where(f => _restoreIds.Contains(f.Id)))
489-
{
490-
SelectedItems.Add(file);
491-
}
492-
}
493483
}
494484

495485
private IQueryable<MediaFile> ApplyFilters(IQueryable<MediaFile> query)
@@ -621,8 +611,15 @@
621611
StateHasChanged();
622612
try
623613
{
624-
await QueueFiles(SelectedItems);
625-
SelectedItems.Clear();
614+
// Selected rows may span pages and reloads, so fetch them fresh.
615+
List<MediaFile> files;
616+
await using (var context = await ContextFactory.CreateDbContextAsync())
617+
{
618+
files = await context.MediaFiles.WithTracksAndProfile().WhereIdsAsync(SelectedIds);
619+
}
620+
621+
await QueueFiles(files);
622+
SelectedIds.Clear();
626623
}
627624
finally
628625
{
@@ -671,12 +668,10 @@
671668
}
672669
}
673670

674-
private Task BatchEditSelected()
671+
private void BatchEditSelected()
675672
{
676-
// SelectedItems is compared by reference, so paging away and back leaves
677-
// stale duplicates of the same row. Go back to the ids.
678-
var ids = SelectedItems.Select(f => f.Id).Distinct().ToList();
679-
return BatchEditTracks(q => q.Where(f => ids.Contains(f.Id)));
673+
// The selection already is the id set the editor needs.
674+
NavigationManager.NavigateTo($"/library/batch?sel={Selections.Put(SelectedIds)}");
680675
}
681676

682677
private Task BatchEditAllFiltered()

Muxarr.Web/Components/Shared/Selection/SelectAllCheckbox.razor

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
@typeparam TType where TType : class
1+
@using Muxarr.Data.Entities
2+
@typeparam TType where TType : class, IHasId
23

34
<div class="form-check">
45
<input type="checkbox"
@@ -10,7 +11,7 @@
1011
@code {
1112
[Parameter] [EditorRequired] public required SelectablePaginatedListComponent<TType> Parent { get; set; }
1213

13-
private bool IsAllSelected => Parent.Items.All(item => Parent.SelectedItems.Contains(item));
14+
private bool IsAllSelected => Parent.Items.All(item => Parent.SelectedIds.Contains(item.Id));
1415

1516
private async Task HandleSelectionChange(ChangeEventArgs args)
1617
{
@@ -21,14 +22,14 @@
2122

2223
if (selected)
2324
{
24-
foreach (var baseEntity in Parent.Items)
25+
foreach (var item in Parent.Items)
2526
{
26-
Parent.SelectedItems.Add(baseEntity);
27+
Parent.SelectedIds.Add(item.Id);
2728
}
2829
}
2930
else
3031
{
31-
Parent.SelectedItems.Clear();
32+
Parent.SelectedIds.Clear();
3233
}
3334

3435
await Parent.OnSelectAll();
Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,20 @@
1-
using Microsoft.AspNetCore.Components;
1+
using Muxarr.Data.Entities;
22

33
namespace Muxarr.Web.Components.Shared.Selection;
44

5-
public abstract class SelectablePaginatedListComponent<T> : PaginatedListComponent<T> where T : class
5+
// Selection is a set of ids, not entity references: rows are reloaded on every
6+
// page or filter change, and two loads of the same row must count as one.
7+
public abstract class SelectablePaginatedListComponent<T> : PaginatedListComponent<T> where T : class, IHasId
68
{
7-
public readonly HashSet<T> SelectedItems = new(new EntityComparer<T>());
8-
9-
[Parameter]
10-
public EventCallback<IEnumerable<T>> OnSelectionChanged { get; set; }
11-
12-
public bool ShowMultiSelect { get; set; }
9+
public readonly HashSet<int> SelectedIds = [];
1310

1411
public async Task OnSelectAll()
1512
{
1613
await InvokeStateHasChanged();
17-
await OnSelectionChanged.InvokeAsync(SelectedItems);
1814
}
1915

2016
public async Task OnSelect()
2117
{
2218
await InvokeStateHasChanged();
23-
await OnSelectionChanged.InvokeAsync(SelectedItems);
24-
}
25-
}
26-
27-
// Todo: check if this works.
28-
public class EntityComparer<T> : IEqualityComparer<T> where T : class
29-
{
30-
public bool Equals(T? x, T? y)
31-
{
32-
if (x == null || y == null)
33-
{
34-
return x == y;
35-
}
36-
37-
return x == y;
38-
}
39-
40-
public int GetHashCode(T obj)
41-
{
42-
return obj.GetHashCode();
4319
}
4420
}

Muxarr.Web/Components/Shared/Selection/SelectionCheckbox.razor

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
@typeparam TType where TType : class
1+
@using Muxarr.Data.Entities
2+
@typeparam TType where TType : class, IHasId
23
<div class="form-check">
34
<input type="checkbox"
45
class="form-check-input"
5-
checked="@Parent.SelectedItems.Contains(Item)"
6+
checked="@Parent.SelectedIds.Contains(Item.Id)"
67
@onchange="HandleSelectionChange"/>
78
</div>
89

@@ -16,11 +17,11 @@
1617
{
1718
if (selected)
1819
{
19-
Parent.SelectedItems.Add(Item);
20+
Parent.SelectedIds.Add(Item.Id);
2021
}
2122
else
2223
{
23-
Parent.SelectedItems.Remove(Item);
24+
Parent.SelectedIds.Remove(Item.Id);
2425
}
2526

2627
await Parent.OnSelect();

0 commit comments

Comments
 (0)