From 8f10e75e5ac5cb3d1d53d770e2d872f84d3ee133 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Thu, 11 Mar 2021 11:52:22 -0600 Subject: [PATCH] multiselect movies, shows, collection items --- .../Commands/AddItemsToCollection.cs | 9 + .../Commands/AddItemsToCollectionHandler.cs | 80 +++++++++ .../Fakes/FakeMediaCollectionRepository.cs | 2 + .../Fakes/FakeTelevisionRepository.cs | 2 + .../IMediaCollectionRepository.cs | 1 + .../Repositories/IMovieRepository.cs | 1 + .../Repositories/ITelevisionRepository.cs | 1 + .../Repositories/MediaCollectionRepository.cs | 28 +++ .../Data/Repositories/MovieRepository.cs | 6 + .../Data/Repositories/TelevisionRepository.cs | 6 + ErsatzTV/Pages/CollectionItems.razor | 127 +++++++++++--- ErsatzTV/Pages/MovieList.razor | 81 ++++++--- ErsatzTV/Pages/MultiSelectBase.cs | 159 ++++++++++++++++++ ErsatzTV/Pages/Search.razor | 63 ++----- ErsatzTV/Pages/TelevisionShowList.razor | 109 ++++++++---- ErsatzTV/Shared/MediaCard.razor | 16 +- ErsatzTV/wwwroot/css/site.css | 6 +- 17 files changed, 564 insertions(+), 133 deletions(-) create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs create mode 100644 ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs create mode 100644 ErsatzTV/Pages/MultiSelectBase.cs diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs new file mode 100644 index 000000000..c0df0030f --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record AddItemsToCollection + (int CollectionId, List MovieIds, List ShowIds) : MediatR.IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs new file mode 100644 index 000000000..835a8001a --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs @@ -0,0 +1,80 @@ +using System.Linq; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using ErsatzTV.Application.Playouts.Commands; +using ErsatzTV.Core; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public class + AddItemsToCollectionHandler : MediatR.IRequestHandler> + { + private readonly ChannelWriter _channel; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly IMovieRepository _movieRepository; + private readonly ITelevisionRepository _televisionRepository; + + public AddItemsToCollectionHandler( + IMediaCollectionRepository mediaCollectionRepository, + IMovieRepository movieRepository, + ITelevisionRepository televisionRepository, + ChannelWriter channel) + { + _mediaCollectionRepository = mediaCollectionRepository; + _movieRepository = movieRepository; + _televisionRepository = televisionRepository; + _channel = channel; + } + + public Task> Handle( + AddItemsToCollection request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(_ => ApplyAddItemsRequest(request)) + .Bind(v => v.ToEitherAsync()); + + private async Task ApplyAddItemsRequest(AddItemsToCollection request) + { + if (await _mediaCollectionRepository.AddMediaItems( + request.CollectionId, + request.MovieIds.Append(request.ShowIds).ToList())) + { + // rebuild all playouts that use this collection + foreach (int playoutId in await _mediaCollectionRepository + .PlayoutIdsUsingCollection(request.CollectionId)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private async Task> Validate(AddItemsToCollection request) => + (await CollectionMustExist(request), await ValidateMovies(request), await ValidateShows(request)) + .Apply((_, _, _) => Unit.Default); + + private Task> CollectionMustExist(AddItemsToCollection request) => + _mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Collection does not exist.")); + + private Task> ValidateMovies(AddItemsToCollection request) => + _movieRepository.AllMoviesExist(request.MovieIds) + .Map(Optional) + .Filter(v => v == true) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Movie does not exist")); + + private Task> ValidateShows(AddItemsToCollection request) => + _televisionRepository.AllShowsExist(request.ShowIds) + .Map(Optional) + .Filter(v => v == true) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Show does not exist")); + } +} diff --git a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs index 2323eaa8b..5a7bd14c6 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs @@ -16,6 +16,8 @@ namespace ErsatzTV.Core.Tests.Fakes public FakeMediaCollectionRepository(Map> data) => _data = data; public Task Add(Collection collection) => throw new NotSupportedException(); public Task AddMediaItem(int collectionId, int mediaItemId) => throw new NotSupportedException(); + public Task AddMediaItems(int collectionId, List mediaItemIds) => throw new NotSupportedException(); + public Task> Get(int id) => throw new NotSupportedException(); public Task> GetCollectionWithItems(int id) => throw new NotSupportedException(); public Task> GetCollectionWithItemsUntracked(int id) => throw new NotSupportedException(); diff --git a/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs index 38782c108..fd7efde37 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs @@ -9,6 +9,8 @@ namespace ErsatzTV.Core.Tests.Fakes { public class FakeTelevisionRepository : ITelevisionRepository { + public Task AllShowsExist(List showIds) => throw new NotSupportedException(); + public Task Update(Show show) => throw new NotSupportedException(); public Task Update(Season season) => throw new NotSupportedException(); diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs index 60ddbc70e..aac17e746 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs @@ -9,6 +9,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories { Task Add(Collection collection); Task AddMediaItem(int collectionId, int mediaItemId); + Task AddMediaItems(int collectionId, List mediaItemIds); Task> Get(int id); Task> GetCollectionWithItems(int id); Task> GetCollectionWithItemsUntracked(int id); diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs index 7b28ea6ac..416168734 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs @@ -7,6 +7,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface IMovieRepository { + Task AllMoviesExist(List movieIds); Task> GetMovie(int movieId); Task> GetOrAdd(LibraryPath libraryPath, string path); Task> GetOrAdd(PlexLibrary library, PlexMovie item); diff --git a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs index dd3287cc1..cfcf7e86a 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs @@ -7,6 +7,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface ITelevisionRepository { + Task AllShowsExist(List showIds); Task Update(Show show); Task Update(Season season); Task Update(Episode episode); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 0a0afeef0..873200338 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -62,6 +62,34 @@ namespace ErsatzTV.Infrastructure.Data.Repositories return modified; } + public async Task AddMediaItems(int collectionId, List mediaItemIds) + { + var modified = false; + + Option maybeCollection = await _dbContext.Collections + .Include(c => c.MediaItems) + .OrderBy(c => c.Id) + .SingleOrDefaultAsync(c => c.Id == collectionId) + .Map(Optional); + + await maybeCollection.IfSomeAsync( + async collection => + { + var toAdd = mediaItemIds.Filter(i => collection.MediaItems.All(i2 => i2.Id != i)).ToList(); + if (toAdd.Any()) + { + List items = await _dbContext.MediaItems + .Filter(mi => toAdd.Contains(mi.Id)) + .ToListAsync(); + + collection.MediaItems.AddRange(items); + modified = await _dbContext.SaveChangesAsync() > 0; + } + }); + + return modified; + } + public Task> Get(int id) => _dbContext.Collections .OrderBy(c => c.Id) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs index d1ba6af9c..c1fcc8240 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs @@ -29,6 +29,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories _dbConnection = dbConnection; } + public Task AllMoviesExist(List movieIds) => + _dbConnection.QuerySingleAsync( + "SELECT COUNT(*) FROM Movie WHERE Id in @MovieIds", + new { MovieIds = movieIds }) + .Map(c => c == movieIds.Count); + public async Task> GetMovie(int movieId) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs index 124f22c74..8332c39d8 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs @@ -24,6 +24,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories _dbConnection = dbConnection; } + public Task AllShowsExist(List showIds) => + _dbConnection.QuerySingleAsync( + "SELECT COUNT(*) FROM Show WHERE Id in @ShowIds", + new { ShowIds = showIds }) + .Map(c => c == showIds.Count); + public async Task Update(Show show) { _dbContext.Shows.Update(show); diff --git a/ErsatzTV/Pages/CollectionItems.razor b/ErsatzTV/Pages/CollectionItems.razor index 314809fdc..eac228745 100644 --- a/ErsatzTV/Pages/CollectionItems.razor +++ b/ErsatzTV/Pages/CollectionItems.razor @@ -2,52 +2,114 @@ @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCards.Queries @using ErsatzTV.Application.MediaCollections.Commands +@inherits MultiSelectBase @inject NavigationManager NavigationManager -@inject IMediator Mediator -@inject ILogger Logger -@inject ISnackbar Snackbar -@inject IDialogService Dialog @inject ChannelWriter Channel - -
- @_data.Name - + +
+ @if (IsSelectMode()) + { + @SelectionLabel() +
+ + Remove From Collection + + + Clear Selection + +
+ } + else + { +
+ @_data.Name + +
+ @if (_data.MovieCards.Any()) + { + @_data.MovieCards.Count Movies + } + @if (_data.ShowCards.Any()) + { + @_data.ShowCards.Count Shows + } + @if (_data.SeasonCards.Any()) + { + @_data.SeasonCards.Count Seasons + } + @if (_data.EpisodeCards.Any()) + { + @_data.EpisodeCards.Count Episodes + } + }
+
+ @if (_data.MovieCards.Any()) { - Movies + + Movies + @foreach (MovieCardViewModel card in _data.MovieCards.OrderBy(m => m.SortTitle)) { + DeleteClicked="@RemoveMovieFromCollection" + SelectColor="@Color.Error" + SelectClicked="@(e => SelectClicked(card, e))" + IsSelected="@IsSelected(card)" + IsSelectMode="@IsSelectMode()"/> } } @if (_data.ShowCards.Any()) { - Television Shows + + Television Shows + @foreach (TelevisionShowCardViewModel card in _data.ShowCards.OrderBy(m => m.SortTitle)) { + DeleteClicked="@RemoveShowFromCollection" + SelectColor="@Color.Error" + SelectClicked="@(e => SelectClicked(card, e))" + IsSelected="@IsSelected(card)" + IsSelectMode="@IsSelectMode()"/> } } @if (_data.SeasonCards.Any()) { - Television Seasons + + Television Seasons + @foreach (TelevisionSeasonCardViewModel card in _data.SeasonCards.OrderBy(m => m.SortTitle)) @@ -56,14 +118,23 @@ Link="@($"/media/tv/seasons/{card.TelevisionSeasonId}")" Title="@card.ShowTitle" Subtitle="@card.Title" - DeleteClicked="@RemoveSeasonFromCollection"/> + DeleteClicked="@RemoveSeasonFromCollection" + SelectColor="@Color.Error" + SelectClicked="@(e => SelectClicked(card, e))" + IsSelected="@IsSelected(card)" + IsSelectMode="@IsSelectMode()"/> } } @if (_data.EpisodeCards.Any()) { - Television Episodes + + Television Episodes + @foreach (TelevisionEpisodeCardViewModel card in _data.EpisodeCards.OrderBy(e => e.Aired)) @@ -75,7 +146,11 @@ ContainerClass="media-card-episode-container mx-2" CardClass="media-card-episode" DeleteClicked="@(_ => RemoveEpisodeFromCollection(card))" - ArtworkKind="@ArtworkKind.Thumbnail"/> + ArtworkKind="@ArtworkKind.Thumbnail" + SelectColor="@Color.Error" + SelectClicked="@(e => SelectClicked(card, e))" + IsSelected="@IsSelected(card)" + IsSelectMode="@IsSelectMode()"/> } } @@ -90,7 +165,7 @@ protected override async Task OnParametersSetAsync() => await RefreshData(); - private async Task RefreshData() + protected override async Task RefreshData() { Either maybeResult = await Mediator.Send(new GetCollectionCards(Id)); @@ -100,6 +175,20 @@ error => NavigationManager.NavigateTo("404")); } + private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) + { + List GetSortedItems() + { + return _data.MovieCards.OrderBy(m => m.SortTitle) + .Append(_data.ShowCards.OrderBy(s => s.SortTitle)) + .Append(_data.SeasonCards.OrderBy(s => s.SortTitle)) + .Append(_data.EpisodeCards.OrderBy(ep => ep.Aired)) + .ToList(); + } + + SelectClicked(GetSortedItems, card, e); + } + private async Task RemoveMovieFromCollection(MediaCardViewModel vm) { if (vm is MovieCardViewModel movie) diff --git a/ErsatzTV/Pages/MovieList.razor b/ErsatzTV/Pages/MovieList.razor index f15e9709d..ec7494933 100644 --- a/ErsatzTV/Pages/MovieList.razor +++ b/ErsatzTV/Pages/MovieList.razor @@ -5,36 +5,61 @@ @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections.Commands @using Unit = LanguageExt.Unit -@inject ILogger Logger -@inject ISnackbar Snackbar -@inject IMediator Mediator -@inject IDialogService Dialog +@inherits MultiSelectBase @inject NavigationManager NavigationManager @inject ChannelWriter Channel - - - - - - - @Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count - - - - - - + +
+ @if (IsSelectMode()) + { + @SelectionLabel() +
+ + Add To Collection + + + Clear Selection + +
+ } + else + { +
+ + + + + @Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count + + + + +
+ } +
+
+ @foreach (MovieCardViewModel card in _data.Cards.Where(m => !string.IsNullOrWhiteSpace(m.Title)).OrderBy(m => m.SortTitle)) { + AddToCollectionClicked="@AddToCollection" + SelectClicked="@(e => SelectClicked(card, e))" + IsSelected="@IsSelected(card)" + IsSelectMode="@IsSelectMode()"/> } @@ -57,13 +82,23 @@ return RefreshData(); } - private async Task RefreshData() => + protected override async Task RefreshData() => _data = await Mediator.Send(new GetMovieCards(PageNumber, PageSize)); private void PrevPage() => NavigationManager.NavigateTo($"/media/movies/page/{PageNumber - 1}"); private void NextPage() => NavigationManager.NavigateTo($"/media/movies/page/{PageNumber + 1}"); + private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) + { + List GetSortedItems() + { + return _data.Cards.OrderBy(m => m.SortTitle).ToList(); + } + + SelectClicked(GetSortedItems, card, e); + } + private async Task AddToCollection(MediaCardViewModel card) { if (card is MovieCardViewModel movie) diff --git a/ErsatzTV/Pages/MultiSelectBase.cs b/ErsatzTV/Pages/MultiSelectBase.cs new file mode 100644 index 000000000..7f577ae55 --- /dev/null +++ b/ErsatzTV/Pages/MultiSelectBase.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ErsatzTV.Application.MediaCards; +using ErsatzTV.Application.MediaCollections; +using ErsatzTV.Application.MediaCollections.Commands; +using ErsatzTV.Core; +using ErsatzTV.Shared; +using LanguageExt; +using LanguageExt.UnsafeValueAccess; +using MediatR; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.Extensions.Logging; +using MudBlazor; +using static LanguageExt.Prelude; +using Unit = LanguageExt.Unit; + +namespace ErsatzTV.Pages +{ + public class MultiSelectBase : FragmentNavigationBase + { + private readonly System.Collections.Generic.HashSet _selectedItems; + private Option _recentlySelected; + + public MultiSelectBase() + { + _recentlySelected = None; + _selectedItems = new System.Collections.Generic.HashSet(); + } + + [Inject] + protected IDialogService Dialog { get; set; } + + [Inject] + protected ISnackbar Snackbar { get; set; } + + [Inject] + protected ILogger Logger { get; set; } + + [Inject] + protected IMediator Mediator { get; set; } + + protected bool IsSelected(MediaCardViewModel card) => + _selectedItems.Contains(card); + + protected bool IsSelectMode() => + _selectedItems.Any(); + + protected string SelectionLabel() => + $"{_selectedItems.Count} {(_selectedItems.Count == 1 ? "Item" : "Items")} Selected"; + + protected void ClearSelection() + { + _selectedItems.Clear(); + _recentlySelected = None; + } + + protected virtual Task RefreshData() => Task.CompletedTask; + + protected void SelectClicked( + Func> getSortedItems, + MediaCardViewModel card, + MouseEventArgs e) + { + if (_selectedItems.Contains(card)) + { + _selectedItems.Remove(card); + } + else + { + if (e.ShiftKey && _recentlySelected.IsSome) + { + List sorted = getSortedItems(); + + int start = sorted.IndexOf(_recentlySelected.ValueUnsafe()); + int finish = sorted.IndexOf(card); + if (start > finish) + { + int temp = start; + start = finish; + finish = temp; + } + + for (int i = start; i < finish; i++) + { + _selectedItems.Add(sorted[i]); + } + } + + _recentlySelected = card; + _selectedItems.Add(card); + } + } + + protected async Task AddSelectionToCollection() + { + var parameters = new DialogParameters + { { "EntityType", _selectedItems.Count.ToString() }, { "EntityName", "selected items" } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Collection", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) + { + var request = new AddItemsToCollection( + collection.Id, + _selectedItems.OfType().Map(m => m.MovieId).ToList(), + _selectedItems.OfType().Map(s => s.TelevisionShowId).ToList()); + + Either addResult = await Mediator.Send(request); + addResult.Match( + Left: error => + { + Snackbar.Add($"Unexpected error adding items to collection: {error.Value}"); + Logger.LogError("Unexpected error adding items to collection: {Error}", error.Value); + }, + Right: _ => + { + Snackbar.Add( + $"Added {_selectedItems.Count} items to collection {collection.Name}", + Severity.Success); + ClearSelection(); + }); + } + } + + protected async Task RemoveSelectionFromCollection(int collectionId) + { + var parameters = new DialogParameters + { { "EntityType", _selectedItems.Count.ToString() }, { "EntityName", "selected items" } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show( + "Remove From Collection", + parameters, + options); + DialogResult result = await dialog.Result; + if (!result.Cancelled) + { + var itemIds = new List(); + itemIds.AddRange(_selectedItems.OfType().Map(m => m.MovieId)); + itemIds.AddRange(_selectedItems.OfType().Map(s => s.TelevisionShowId)); + itemIds.AddRange(_selectedItems.OfType().Map(s => s.TelevisionSeasonId)); + itemIds.AddRange(_selectedItems.OfType().Map(e => e.EpisodeId)); + + await Mediator.Send( + new RemoveItemsFromCollection(collectionId) + { + MediaItemIds = itemIds + }); + + await RefreshData(); + ClearSelection(); + } + } + } +} diff --git a/ErsatzTV/Pages/Search.razor b/ErsatzTV/Pages/Search.razor index 891f8ea54..62f9a1877 100644 --- a/ErsatzTV/Pages/Search.razor +++ b/ErsatzTV/Pages/Search.razor @@ -6,23 +6,20 @@ @using Microsoft.AspNetCore.WebUtilities @using Microsoft.Extensions.Primitives @using Unit = LanguageExt.Unit -@inherits FragmentNavigationBase +@inherits MultiSelectBase @inject NavigationManager NavigationManager -@inject IMediator Mediator -@inject ILogger Logger -@inject ISnackbar Snackbar -@inject IDialogService Dialog @inject ChannelWriter Channel - -
- @if (_selectedCards.Any()) + +
+ @if (IsSelectMode()) { - @_selectedCards.Count @(_selectedCards.Count == 1 ? "Item" : "Items") Selected + @SelectionLabel()
+ StartIcon="@Icons.Material.Filled.Add" + OnClick="@(_ => AddSelectionToCollection())"> Add To Collection + IsSelected="@IsSelected(card)" + IsSelectMode="@IsSelectMode()"/> } } @@ -81,8 +78,8 @@ Link="@($"/media/tv/shows/{card.TelevisionShowId}")" AddToCollectionClicked="@AddToCollection" SelectClicked="@(e => SelectClicked(card, e))" - IsSelected="@_selectedCards.Contains(card)" - IsSelectMode="@_selectedCards.Any()"/> + IsSelected="@IsSelected(card)" + IsSelectMode="@IsSelectMode()"/> } } @@ -91,8 +88,6 @@ @code { private string _query; private SearchCardResultsViewModel _data; - private System.Collections.Generic.HashSet _selectedCards; - private MediaCardViewModel _selectedLast; protected override async Task OnInitializedAsync() { @@ -104,44 +99,18 @@ Either maybeResults = await Mediator.Send(new GetSearchCards(_query)); maybeResults.IfRight(results => _data = results); } - - _selectedCards = new System.Collections.Generic.HashSet(); } - private void ClearSelection() => _selectedCards.Clear(); - private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { - if (_selectedCards.Contains(card)) + List GetSortedItems() { - _selectedCards.Remove(card); + return _data.MovieCards.OrderBy(m => m.SortTitle) + .Append(_data.ShowCards.OrderBy(s => s.SortTitle)) + .ToList(); } - else - { - if (e.ShiftKey && _selectedLast != null) - { - var sorted = _data.MovieCards.OrderBy(m => m.SortTitle) - .Append(_data.ShowCards.OrderBy(s => s.SortTitle)) - .ToList(); - int start = sorted.IndexOf(_selectedLast); - int finish = sorted.IndexOf(card); - if (start > finish) - { - int temp = start; - start = finish; - finish = temp; - } - - for (int i = start; i < finish; i++) - { - _selectedCards.Add(sorted[i]); - } - } - - _selectedLast = card; - _selectedCards.Add(card); - } + SelectClicked(GetSortedItems, card, e); } private async Task AddToCollection(MediaCardViewModel card) diff --git a/ErsatzTV/Pages/TelevisionShowList.razor b/ErsatzTV/Pages/TelevisionShowList.razor index 2d60cda66..952216394 100644 --- a/ErsatzTV/Pages/TelevisionShowList.razor +++ b/ErsatzTV/Pages/TelevisionShowList.razor @@ -1,63 +1,102 @@ @page "/media/tv/shows" +@page "/media/tv/shows/page/{PageNumber:int}" @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCards.Queries @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections.Commands @using Unit = LanguageExt.Unit -@inject ILogger Logger -@inject ISnackbar Snackbar -@inject IMediator Mediator -@inject IDialogService Dialog +@inherits MultiSelectBase +@inject NavigationManager NavigationManager @inject ChannelWriter Channel - - - - - - - @Math.Min((_pageNumber - 1) * _pageSize + 1, _data.Count)-@Math.Min(_data.Count, _pageNumber * _pageSize) of @_data.Count - - - - - - + +
+ @if (IsSelectMode()) + { + @SelectionLabel() +
+ + Add To Collection + + + Clear Selection + +
+ } + else + { +
+ + + + + @Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count + + + + +
+ } +
+
+ @foreach (TelevisionShowCardViewModel card in _data.Cards.OrderBy(s => s.SortTitle)) { + AddToCollectionClicked="@AddToCollection" + SelectClicked="@(e => SelectClicked(card, e))" + IsSelected="@IsSelected(card)" + IsSelectMode="@IsSelectMode()"/> } @code { - private int _pageSize => 100; - private int _pageNumber = 1; - - private TelevisionShowCardResultsViewModel _data; + private static int PageSize => 100; - protected override Task OnParametersSetAsync() => RefreshData(); + [Parameter] + public int PageNumber { get; set; } - private async Task RefreshData() => - _data = await Mediator.Send(new GetTelevisionShowCards(_pageNumber, _pageSize)); + private TelevisionShowCardResultsViewModel _data; - private async Task PrevPage() + protected override Task OnParametersSetAsync() { - _pageNumber -= 1; - await RefreshData(); + if (PageNumber == 0) + { + PageNumber = 1; + } + + return RefreshData(); } - private async Task NextPage() + protected override async Task RefreshData() => + _data = await Mediator.Send(new GetTelevisionShowCards(PageNumber, PageSize)); + + private void PrevPage() => NavigationManager.NavigateTo($"/media/tv/shows/page/{PageNumber - 1}"); + + private void NextPage() => NavigationManager.NavigateTo($"/media/tv/shows/page/{PageNumber + 1}"); + + private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { - _pageNumber += 1; - await RefreshData(); + List GetSortedItems() + { + return _data.Cards.OrderBy(m => m.SortTitle).ToList(); + } + + SelectClicked(GetSortedItems, card, e); } private async Task AddToCollection(MediaCardViewModel card) diff --git a/ErsatzTV/Shared/MediaCard.razor b/ErsatzTV/Shared/MediaCard.razor index 217874789..7adc7faad 100644 --- a/ErsatzTV/Shared/MediaCard.razor +++ b/ErsatzTV/Shared/MediaCard.razor @@ -5,7 +5,8 @@
@if (!string.IsNullOrWhiteSpace(Link)) { -
+
@if (string.IsNullOrWhiteSpace(Data.Poster)) { @@ -16,8 +17,8 @@ @if (IsSelected) { -
- +
@@ -29,9 +30,9 @@ @if (SelectClicked.HasDelegate) { - } @if (AddToCollectionClicked.HasDelegate && !IsSelectMode) @@ -43,7 +44,7 @@ } @if (DeleteClicked.HasDelegate && !IsSelectMode) { - @@ -114,6 +115,9 @@ [Parameter] public bool IsSelected { get; set; } + [Parameter] + public Color SelectColor { get; set; } = Color.Tertiary; + private string GetPlaceholder(string sortTitle) { if (Placeholder != null) diff --git a/ErsatzTV/wwwroot/css/site.css b/ErsatzTV/wwwroot/css/site.css index 64e0a4d4b..da09f2f36 100644 --- a/ErsatzTV/wwwroot/css/site.css +++ b/ErsatzTV/wwwroot/css/site.css @@ -18,9 +18,9 @@ width: 152px; } -.media-card-selected { - box-shadow: 0 0 0 3px #00c000, 0 0 4px rgba(0, 0, 0, 0.3); -} +.media-card-selected { box-shadow: 0 0 0 3px #00c000, 0 0 4px rgba(0, 0, 0, 0.3); } + +.media-card-selected-delete { box-shadow: 0 0 0 3px #f44336, 0 0 4px rgba(0, 0, 0, 0.3); } .media-card-episode { width: 392px; }