From 7a4c83215699d4f03d4fb61bb520c001a80160ab Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:00:39 +0000 Subject: [PATCH] add media card overflow menu (#2180) * add media card overflow menu * remove commented code --- CHANGELOG.md | 2 + .../MediaCards/ActorCardViewModel.cs | 2 +- .../MediaCards/ArtistCardViewModel.cs | 3 +- .../MediaCards/ImageCardViewModel.cs | 3 +- .../MediaCards/MediaCardViewModel.cs | 3 +- .../MediaCards/MovieCardViewModel.cs | 3 +- .../MediaCards/MusicVideoCardViewModel.cs | 3 +- .../MediaCards/OtherVideoCardViewModel.cs | 3 +- .../MediaCards/RemoteStreamCardViewModel.cs | 3 +- .../MediaCards/SongCardViewModel.cs | 3 +- .../TelevisionEpisodeCardViewModel.cs | 3 +- .../TelevisionSeasonCardViewModel.cs | 3 +- .../MediaCards/TelevisionShowCardViewModel.cs | 3 +- .../MediaCollectionViewModel.cs | 3 +- .../Queries/GetMediaItemInfoHandler.cs | 16 ++-- ErsatzTV/Shared/MediaCard.razor | 84 +++++++++++++++---- ErsatzTV/wwwroot/css/site.css | 13 +-- 17 files changed, 107 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41069272..e1060cc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - `Playlist Item Size`: will play all media items from the current playlist item before continuing to the next schedule item - `Multi-Episode Group Size`: will play all media items from the current multi-part episode group, or one ungrouped media item - Change watermark width and margins to allow decimals +- Move `Add To Collection` button to overflow menu on all media cards, and add `Show Media Info` to overflow menu + - This allows showing media info for all media kinds ### Fixed - Fix QSV acceleration in docker with older Intel devices diff --git a/ErsatzTV.Application/MediaCards/ActorCardViewModel.cs b/ErsatzTV.Application/MediaCards/ActorCardViewModel.cs index 9e99f652..e92c7fd8 100644 --- a/ErsatzTV.Application/MediaCards/ActorCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/ActorCardViewModel.cs @@ -3,4 +3,4 @@ namespace ErsatzTV.Application.MediaCards; public record ActorCardViewModel(int Id, string Name, string Role, string Thumb, MediaItemState State) : - MediaCardViewModel(Id, Name, Role, Name, Thumb, State); + MediaCardViewModel(Id, Name, Role, Name, Thumb, State, HasMediaInfo: false); diff --git a/ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs b/ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs index 1722c7ab..ef1c5f4f 100644 --- a/ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs @@ -14,4 +14,5 @@ public record ArtistCardViewModel( Subtitle, SortTitle, Poster, - State); + State, + HasMediaInfo: false); diff --git a/ErsatzTV.Application/MediaCards/ImageCardViewModel.cs b/ErsatzTV.Application/MediaCards/ImageCardViewModel.cs index 6442558d..2b0df426 100644 --- a/ErsatzTV.Application/MediaCards/ImageCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/ImageCardViewModel.cs @@ -14,7 +14,8 @@ public record ImageCardViewModel( Subtitle, SortTitle, Poster, - State) + State, + HasMediaInfo: true) { public int CustomIndex { get; set; } } diff --git a/ErsatzTV.Application/MediaCards/MediaCardViewModel.cs b/ErsatzTV.Application/MediaCards/MediaCardViewModel.cs index 0db9f192..c5f06e33 100644 --- a/ErsatzTV.Application/MediaCards/MediaCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/MediaCardViewModel.cs @@ -8,4 +8,5 @@ public record MediaCardViewModel( string Subtitle, string SortTitle, string Poster, - MediaItemState State); + MediaItemState State, + bool HasMediaInfo); diff --git a/ErsatzTV.Application/MediaCards/MovieCardViewModel.cs b/ErsatzTV.Application/MediaCards/MovieCardViewModel.cs index 3e0db59e..a725b80d 100644 --- a/ErsatzTV.Application/MediaCards/MovieCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/MovieCardViewModel.cs @@ -14,7 +14,8 @@ public record MovieCardViewModel( Subtitle, SortTitle, Poster, - State) + State, + HasMediaInfo: true) { public int CustomIndex { get; set; } } diff --git a/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs b/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs index baa4694d..8df9c471 100644 --- a/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs @@ -18,7 +18,8 @@ public record MusicVideoCardViewModel( Subtitle, SortTitle, Poster, - State) + State, + HasMediaInfo: true) { public int CustomIndex { get; set; } } diff --git a/ErsatzTV.Application/MediaCards/OtherVideoCardViewModel.cs b/ErsatzTV.Application/MediaCards/OtherVideoCardViewModel.cs index eb2a34f6..6ec0e0bf 100644 --- a/ErsatzTV.Application/MediaCards/OtherVideoCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/OtherVideoCardViewModel.cs @@ -14,7 +14,8 @@ public record OtherVideoCardViewModel( Subtitle, SortTitle, Poster, - State) + State, + HasMediaInfo: true) { public int CustomIndex { get; set; } } diff --git a/ErsatzTV.Application/MediaCards/RemoteStreamCardViewModel.cs b/ErsatzTV.Application/MediaCards/RemoteStreamCardViewModel.cs index 9b5e5f39..c0f6e969 100644 --- a/ErsatzTV.Application/MediaCards/RemoteStreamCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/RemoteStreamCardViewModel.cs @@ -14,7 +14,8 @@ public record RemoteStreamCardViewModel( Subtitle, SortTitle, Poster, - State) + State, + HasMediaInfo: true) { public int CustomIndex { get; set; } } diff --git a/ErsatzTV.Application/MediaCards/SongCardViewModel.cs b/ErsatzTV.Application/MediaCards/SongCardViewModel.cs index 04a15ac9..59eea83a 100644 --- a/ErsatzTV.Application/MediaCards/SongCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/SongCardViewModel.cs @@ -14,7 +14,8 @@ public record SongCardViewModel( Subtitle, SortTitle, Poster, - State) + State, + HasMediaInfo: true) { public int CustomIndex { get; set; } } diff --git a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs index 394d1109..e77296cd 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs @@ -24,4 +24,5 @@ public record TelevisionEpisodeCardViewModel( $"Episode {Episode}", SortTitle, Poster, - State); + State, + HasMediaInfo: true); diff --git a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs index e51a5bf8..186f9545 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs @@ -17,4 +17,5 @@ public record TelevisionSeasonCardViewModel( Subtitle, SortTitle, Poster, - State); + State, + HasMediaInfo: false); diff --git a/ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs index e6f77cd0..c7c3f14a 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs @@ -14,4 +14,5 @@ public record TelevisionShowCardViewModel( Subtitle, SortTitle, Poster, - State); + State, + HasMediaInfo: false); diff --git a/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs b/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs index a30f7275..74a26f80 100644 --- a/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs @@ -14,4 +14,5 @@ public record MediaCollectionViewModel( string.Empty, Name, string.Empty, - State); + State, + HasMediaInfo: false); diff --git a/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs b/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs index 0a402878..4214383b 100644 --- a/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs +++ b/ErsatzTV.Application/MediaItems/Queries/GetMediaItemInfoHandler.cs @@ -30,7 +30,7 @@ public class GetMediaItemInfoHandler : IRequestHandler l.MediaSource) // TODO: support all media types here .Include(i => (i as Movie).MovieMetadata) - .ThenInclude(mv => mv.Subtitles) + .ThenInclude(mm => mm.Subtitles) .Include(i => (i as Movie).MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(i => (i as Movie).MediaVersions) @@ -40,40 +40,42 @@ public class GetMediaItemInfoHandler : IRequestHandler (i as Episode).MediaVersions) .ThenInclude(mv => mv.Streams) .Include(i => (i as Episode).EpisodeMetadata) - .ThenInclude(mv => mv.Subtitles) + .ThenInclude(em => em.Subtitles) .Include(i => (i as Episode).Season) .ThenInclude(s => s.Show) .ThenInclude(s => s.ShowMetadata) .Include(i => (i as OtherVideo).OtherVideoMetadata) - .ThenInclude(mv => mv.Subtitles) + .ThenInclude(ovm => ovm.Subtitles) .Include(i => (i as OtherVideo).MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(i => (i as OtherVideo).MediaVersions) .ThenInclude(mv => mv.Streams) .Include(i => (i as Image).ImageMetadata) - .ThenInclude(mv => mv.Subtitles) + .ThenInclude(im => im.Subtitles) .Include(i => (i as Image).MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(i => (i as Image).MediaVersions) .ThenInclude(mv => mv.Streams) .Include(i => (i as RemoteStream).RemoteStreamMetadata) - .ThenInclude(mv => mv.Subtitles) + .ThenInclude(rsm => rsm.Subtitles) .Include(i => (i as RemoteStream).MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(i => (i as RemoteStream).MediaVersions) .ThenInclude(mv => mv.Streams) .Include(i => (i as Song).SongMetadata) - .ThenInclude(mv => mv.Subtitles) + .ThenInclude(sm => sm.Subtitles) .Include(i => (i as Song).MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(i => (i as Song).MediaVersions) .ThenInclude(mv => mv.Streams) .Include(i => (i as MusicVideo).MusicVideoMetadata) - .ThenInclude(mv => mv.Subtitles) + .ThenInclude(mvm => mvm.Subtitles) .Include(i => (i as MusicVideo).MediaVersions) .ThenInclude(mv => mv.Chapters) .Include(i => (i as MusicVideo).MediaVersions) .ThenInclude(mv => mv.Streams) + .Include(i => (i as MusicVideo).Artist) + .ThenInclude(a => a.ArtistMetadata) .SelectOneAsync(i => i.Id, i => i.Id == request.Id) .MapT(Project); diff --git a/ErsatzTV/Shared/MediaCard.razor b/ErsatzTV/Shared/MediaCard.razor index 1a1134e6..9de5387f 100644 --- a/ErsatzTV/Shared/MediaCard.razor +++ b/ErsatzTV/Shared/MediaCard.razor @@ -1,6 +1,9 @@ @using ErsatzTV.Application.MediaCards +@using ErsatzTV.Application.MediaItems @using static Prelude @inject NavigationManager NavigationManager +@inject IDialogService Dialog +@inject IMediator Mediator
@if (SelectClicked.HasDelegate || !string.IsNullOrWhiteSpace(Href)) @@ -35,32 +38,53 @@
} -
- - + + @if (SelectClicked.HasDelegate) { } - @if (AddToCollectionClicked.HasDelegate && !IsSelectMode) + @if (!IsSelectMode) { - + @if (AddToCollectionClicked.HasDelegate || AddToPlaylistClicked.HasDelegate || Data.HasMediaInfo) + { + + @if (AddToCollectionClicked.HasDelegate) + { + + } + @if (AddToPlaylistClicked.HasDelegate) + { + + } + @if (Data.HasMediaInfo) + { + + } + + } + + @if (DeleteClicked.HasDelegate) + { + + } } - @if (DeleteClicked.HasDelegate && !IsSelectMode) - { - - } -
+ } else @@ -117,6 +141,9 @@ [Parameter] public EventCallback AddToCollectionClicked { get; set; } + [Parameter] + public EventCallback AddToPlaylistClicked { get; set; } + [Parameter] public EventCallback SelectClicked { get; set; } @@ -132,6 +159,8 @@ [Parameter] public bool IsRemoteArtwork { get; set; } + private string _overlayClass = "media-card-overlay"; + private string GetPlaceholder(string sortTitle) { if (Placeholder != null) @@ -173,4 +202,25 @@ } } + private void OnMenuOpenChanged(bool open) + { + _overlayClass = open + ? "media-card-overlay media-card-overlay-menu-open" + : "media-card-overlay"; + + StateHasChanged(); + } + + private async Task ShowMediaInfo() + { + Either maybeInfo = await Mediator.Send(new GetMediaItemInfo(Data.MediaItemId)); + foreach (MediaItemInfo info in maybeInfo.RightToSeq()) + { + var parameters = new DialogParameters { { "MediaItemInfo", info } }; + var options = new DialogOptions { CloseButton = true, CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; + IDialogReference dialog = await Dialog.ShowAsync(Data.Title, parameters, options); + DialogResult _ = await dialog.Result; + } + } + } \ No newline at end of file diff --git a/ErsatzTV/wwwroot/css/site.css b/ErsatzTV/wwwroot/css/site.css index 990a930c..f9b30604 100644 --- a/ErsatzTV/wwwroot/css/site.css +++ b/ErsatzTV/wwwroot/css/site.css @@ -31,15 +31,6 @@ .media-card-poster-placeholder { font-weight: bold; } -.media-card-menu { - bottom: 0; - display: none; - position: absolute; - right: 0; -} - -.media-card:hover .media-card-menu { display: block; } - .media-card-overlay { background: rgba(0, 0, 0, 0.4); border-radius: 4px; @@ -54,6 +45,10 @@ .media-card-overlay:hover { opacity: 1; } +.media-card-overlay:not(:hover) button.media-card-select-checkbox { opacity: 0; } + +.media-card-overlay-menu-open { opacity: 1; } + .search-bar .mud-input { height: 42px; } .search-bar {