diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f64af0f..7e4489472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - `ETV_SLOW_API_MS` - milliseconds threshold for logging slow API calls (at DEBUG level) - This is currently limited to *Jellyfin* - `ETV_JF_PAGE_SIZE` - page size for library scan API calls to Jellyfin; default value is 10 +- Add `Select All` button to media pages by @Erotemic ### Fixed - Fix startup on systems unsupported by NvEncSharp diff --git a/ErsatzTV.Tests/ErsatzTV.Tests.csproj b/ErsatzTV.Tests/ErsatzTV.Tests.csproj new file mode 100644 index 000000000..66f7b764b --- /dev/null +++ b/ErsatzTV.Tests/ErsatzTV.Tests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + diff --git a/ErsatzTV.Tests/Pages/MultiSelectBaseTests.cs b/ErsatzTV.Tests/Pages/MultiSelectBaseTests.cs new file mode 100644 index 000000000..b31ecc38c --- /dev/null +++ b/ErsatzTV.Tests/Pages/MultiSelectBaseTests.cs @@ -0,0 +1,35 @@ +using ErsatzTV.Application.MediaCards; +using ErsatzTV.Pages; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Tests.Pages; + +[TestFixture] +public class MultiSelectBaseTests +{ + [Test] + public void Should_replace_existing_selection_and_return_last_card() + { + var existingCard = new MediaCardViewModel(1, "Existing", "Sub", "Existing", "", Core.Domain.MediaItemState.Normal, false); + var selected = new HashSet { existingCard }; + + var first = new MediaCardViewModel(2, "First", "Sub", "First", "", Core.Domain.MediaItemState.Normal, false); + var second = new MediaCardViewModel(3, "Second", "Sub", "Second", "", Core.Domain.MediaItemState.Normal, false); + + MultiSelectBase.ResetSelectionWithCards(selected, [first, second]); + + selected.ShouldBe([first, second], ignoreOrder: true); + } + + [Test] + public void Should_clear_selection_when_no_cards() + { + var existingCard = new MediaCardViewModel(1, "Existing", "Sub", "Existing", "", Core.Domain.MediaItemState.Normal, false); + var selected = new HashSet { existingCard }; + + MultiSelectBase.ResetSelectionWithCards(selected, []); + + selected.ShouldBeEmpty(); + } +} diff --git a/ErsatzTV.sln b/ErsatzTV.sln index f81b2667b..d1d9dd8b4 100644 --- a/ErsatzTV.sln +++ b/ErsatzTV.sln @@ -28,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErsatzTV.Infrastructure.Sql EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErsatzTV.Core.Nullable", "ErsatzTV.Core.Nullable\ErsatzTV.Core.Nullable.csproj", "{557D88A6-C982-4FFD-8FD2-6446CB07D093}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErsatzTV.Tests", "ErsatzTV.Tests\ErsatzTV.Tests.csproj", "{56F56E76-CEF4-4639-B7BB-03FD201BB019}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -113,11 +115,18 @@ Global {557D88A6-C982-4FFD-8FD2-6446CB07D093}.Release|Any CPU.Build.0 = Release|Any CPU {557D88A6-C982-4FFD-8FD2-6446CB07D093}.Debug No Sync|Any CPU.ActiveCfg = Debug|Any CPU {557D88A6-C982-4FFD-8FD2-6446CB07D093}.Debug No Sync|Any CPU.Build.0 = Debug|Any CPU + {56F56E76-CEF4-4639-B7BB-03FD201BB019}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56F56E76-CEF4-4639-B7BB-03FD201BB019}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56F56E76-CEF4-4639-B7BB-03FD201BB019}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56F56E76-CEF4-4639-B7BB-03FD201BB019}.Release|Any CPU.Build.0 = Release|Any CPU + {56F56E76-CEF4-4639-B7BB-03FD201BB019}.Debug No Sync|Any CPU.ActiveCfg = Debug|Any CPU + {56F56E76-CEF4-4639-B7BB-03FD201BB019}.Debug No Sync|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CE7F1ACD-F286-4761-A7BC-A541A1E25C86} = {325E6DA0-52B3-4431-98A2-72C36F403704} {1C892530-CF92-4F43-8C64-BCEEF958D726} = {325E6DA0-52B3-4431-98A2-72C36F403704} {591FB3F4-4DD8-441B-B7C8-F2A42BF69992} = {325E6DA0-52B3-4431-98A2-72C36F403704} {2EF80455-953D-4696-831D-E8CBCA82B0EF} = {325E6DA0-52B3-4431-98A2-72C36F403704} + {56F56E76-CEF4-4639-B7BB-03FD201BB019} = {325E6DA0-52B3-4431-98A2-72C36F403704} EndGlobalSection EndGlobal diff --git a/ErsatzTV/Pages/ArtistList.razor b/ErsatzTV/Pages/ArtistList.razor index c3e29ce36..848e21c90 100644 --- a/ErsatzTV/Pages/ArtistList.razor +++ b/ErsatzTV/Pages/ArtistList.razor @@ -18,6 +18,8 @@ AddSelectionToCollection="@AddSelectionToCollection" AddSelectionToPlaylist="@AddSelectionToPlaylist" ClearSelection="@ClearSelection" + CanSelectAllOnPage="@(() => _data.Cards.Count > 0)" + SelectAllOnPage="@(() => SelectAllPageItems(_data.Cards))" IsSelectMode="@IsSelectMode" SelectionLabel="@SelectionLabel"/> diff --git a/ErsatzTV/Pages/EpisodeList.razor b/ErsatzTV/Pages/EpisodeList.razor index d2d7e18db..92747e8f0 100644 --- a/ErsatzTV/Pages/EpisodeList.razor +++ b/ErsatzTV/Pages/EpisodeList.razor @@ -18,6 +18,8 @@ AddSelectionToCollection="@AddSelectionToCollection" AddSelectionToPlaylist="@AddSelectionToPlaylist" ClearSelection="@ClearSelection" + CanSelectAllOnPage="@(() => _data.Cards.Count > 0)" + SelectAllOnPage="@(() => SelectAllPageItems(_data.Cards))" IsSelectMode="@IsSelectMode" SelectionLabel="@SelectionLabel"/> diff --git a/ErsatzTV/Pages/ImageList.razor b/ErsatzTV/Pages/ImageList.razor index c9b1f1646..4ff230fbd 100644 --- a/ErsatzTV/Pages/ImageList.razor +++ b/ErsatzTV/Pages/ImageList.razor @@ -18,6 +18,8 @@ AddSelectionToCollection="@AddSelectionToCollection" AddSelectionToPlaylist="@AddSelectionToPlaylist" ClearSelection="@ClearSelection" + CanSelectAllOnPage="@(() => _data.Cards.Count > 0)" + SelectAllOnPage="@(() => SelectAllPageItems(_data.Cards))" IsSelectMode="@IsSelectMode" SelectionLabel="@SelectionLabel"/> diff --git a/ErsatzTV/Pages/MovieList.razor b/ErsatzTV/Pages/MovieList.razor index 9c4cadf9b..0b4c5310c 100644 --- a/ErsatzTV/Pages/MovieList.razor +++ b/ErsatzTV/Pages/MovieList.razor @@ -20,6 +20,8 @@ AddSelectionToCollection="@AddSelectionToCollection" AddSelectionToPlaylist="@AddSelectionToPlaylist" ClearSelection="@ClearSelection" + CanSelectAllOnPage="@(() => _data.Cards.Count > 0)" + SelectAllOnPage="@(() => SelectAllPageItems(_data.Cards))" IsSelectMode="@IsSelectMode" SelectionLabel="@SelectionLabel"/> diff --git a/ErsatzTV/Pages/MultiSelectBase.cs b/ErsatzTV/Pages/MultiSelectBase.cs index bb2ed5608..eb9fe1fed 100644 --- a/ErsatzTV/Pages/MultiSelectBase.cs +++ b/ErsatzTV/Pages/MultiSelectBase.cs @@ -38,6 +38,12 @@ public class MultiSelectBase : FragmentNavigationBase protected string SelectionLabel() => $"{SelectedItems.Count} {(SelectedItems.Count == 1 ? "Item" : "Items")} Selected"; + protected void SelectAllPageItems(IEnumerable cards) + { + ResetSelectionWithCards(SelectedItems, cards); + StateHasChanged(); + } + protected void ClearSelection() { SelectedItems.Clear(); @@ -239,4 +245,12 @@ public class MultiSelectBase : FragmentNavigationBase }); } } + + internal static void ResetSelectionWithCards( + ISet selectedItems, + IEnumerable cards) + { + selectedItems.Clear(); + selectedItems.UnionWith(cards); + } } diff --git a/ErsatzTV/Pages/MusicVideoList.razor b/ErsatzTV/Pages/MusicVideoList.razor index 4a7a1dc98..24a2590bb 100644 --- a/ErsatzTV/Pages/MusicVideoList.razor +++ b/ErsatzTV/Pages/MusicVideoList.razor @@ -18,6 +18,8 @@ AddSelectionToCollection="@AddSelectionToCollection" AddSelectionToPlaylist="@AddSelectionToPlaylist" ClearSelection="@ClearSelection" + CanSelectAllOnPage="@(() => _data.Cards.Count > 0)" + SelectAllOnPage="@(() => SelectAllPageItems(_data.Cards))" IsSelectMode="@IsSelectMode" SelectionLabel="@SelectionLabel"/> diff --git a/ErsatzTV/Pages/OtherVideoList.razor b/ErsatzTV/Pages/OtherVideoList.razor index bb4509216..ebc1cbe57 100644 --- a/ErsatzTV/Pages/OtherVideoList.razor +++ b/ErsatzTV/Pages/OtherVideoList.razor @@ -18,6 +18,8 @@ AddSelectionToCollection="@AddSelectionToCollection" AddSelectionToPlaylist="@AddSelectionToPlaylist" ClearSelection="@ClearSelection" + CanSelectAllOnPage="@(() => _data.Cards.Count > 0)" + SelectAllOnPage="@(() => SelectAllPageItems(_data.Cards))" IsSelectMode="@IsSelectMode" SelectionLabel="@SelectionLabel"/> diff --git a/ErsatzTV/Pages/RemoteStreamList.razor b/ErsatzTV/Pages/RemoteStreamList.razor index d2d78bd1f..9131c07a9 100644 --- a/ErsatzTV/Pages/RemoteStreamList.razor +++ b/ErsatzTV/Pages/RemoteStreamList.razor @@ -18,6 +18,8 @@ AddSelectionToCollection="@AddSelectionToCollection" AddSelectionToPlaylist="@AddSelectionToPlaylist" ClearSelection="@ClearSelection" + CanSelectAllOnPage="@(() => _data.Cards.Count > 0)" + SelectAllOnPage="@(() => SelectAllPageItems(_data.Cards))" IsSelectMode="@IsSelectMode" SelectionLabel="@SelectionLabel"/> diff --git a/ErsatzTV/Pages/SongList.razor b/ErsatzTV/Pages/SongList.razor index b7d2b071e..c0b13cd92 100644 --- a/ErsatzTV/Pages/SongList.razor +++ b/ErsatzTV/Pages/SongList.razor @@ -18,6 +18,8 @@ AddSelectionToCollection="@AddSelectionToCollection" AddSelectionToPlaylist="@AddSelectionToPlaylist" ClearSelection="@ClearSelection" + CanSelectAllOnPage="@(() => _data.Cards.Count > 0)" + SelectAllOnPage="@(() => SelectAllPageItems(_data.Cards))" IsSelectMode="@IsSelectMode" SelectionLabel="@SelectionLabel"/> diff --git a/ErsatzTV/Pages/TelevisionShowList.razor b/ErsatzTV/Pages/TelevisionShowList.razor index 45c73b94e..c23f36faf 100644 --- a/ErsatzTV/Pages/TelevisionShowList.razor +++ b/ErsatzTV/Pages/TelevisionShowList.razor @@ -20,6 +20,8 @@ AddSelectionToCollection="@AddSelectionToCollection" AddSelectionToPlaylist="@AddSelectionToPlaylist" ClearSelection="@ClearSelection" + CanSelectAllOnPage="@(() => _data.Cards.Count > 0)" + SelectAllOnPage="@(() => SelectAllPageItems(_data.Cards))" IsSelectMode="@IsSelectMode" SelectionLabel="@SelectionLabel"/> diff --git a/ErsatzTV/Pages/Trash.razor b/ErsatzTV/Pages/Trash.razor index dfb1d241e..81e3f44d0 100644 --- a/ErsatzTV/Pages/Trash.razor +++ b/ErsatzTV/Pages/Trash.razor @@ -92,25 +92,37 @@ @_remoteStreams.Count Remote Streams } - - - Empty Trash - - + + Select All + + + Empty Trash + - - - Empty Trash - - + + Select All + + + Empty Trash + } else @@ -509,7 +521,20 @@ } private bool IsNotEmpty => - _movies?.Cards.Count > 0 || _shows?.Cards.Count > 0 || _seasons?.Cards.Count > 0 || _episodes?.Cards.Count > 0 || _musicVideos?.Cards.Count > 0 || _otherVideos?.Cards.Count > 0 || _songs?.Cards.Count > 0 || _artists?.Cards.Count > 0 || _images?.Cards.Count > 0; + _movies?.Cards.Count > 0 || _shows?.Cards.Count > 0 || _seasons?.Cards.Count > 0 || _episodes?.Cards.Count > 0 || _musicVideos?.Cards.Count > 0 || _otherVideos?.Cards.Count > 0 || _songs?.Cards.Count > 0 || _artists?.Cards.Count > 0 || _images?.Cards.Count > 0 || _remoteStreams?.Cards.Count > 0; + + private IEnumerable AllCardsOnPage() => + Enumerable.Empty() + .Concat(_movies.Cards) + .Concat(_shows.Cards) + .Concat(_seasons.Cards) + .Concat(_episodes.Cards) + .Concat(_artists.Cards) + .Concat(_musicVideos.Cards) + .Concat(_otherVideos.Cards) + .Concat(_songs.Cards) + .Concat(_images.Cards) + .Concat(_remoteStreams.Cards); private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { diff --git a/ErsatzTV/Properties/AssemblyInfo.cs b/ErsatzTV/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..9a243bed4 --- /dev/null +++ b/ErsatzTV/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ErsatzTV.Tests")] diff --git a/ErsatzTV/Shared/MediaCardPager.razor b/ErsatzTV/Shared/MediaCardPager.razor index 46658b280..d4ffb24fa 100644 --- a/ErsatzTV/Shared/MediaCardPager.razor +++ b/ErsatzTV/Shared/MediaCardPager.razor @@ -44,7 +44,7 @@ @Query - + - + + @if (SelectAllOnPage is not null) + { + + + Select All + + + + + + + + } + } @@ -99,6 +120,12 @@ [Parameter] public Action ClearSelection { get; set; } + [Parameter] + public Func CanSelectAllOnPage { get; set; } + + [Parameter] + public Action SelectAllOnPage { get; set; } + private static MarkupString PaddedString(int value, int maximum) { int length = maximum.ToString(CultureInfo.InvariantCulture).Length;