add media card overflow menu (#2180)

* add media card overflow menu

* remove commented code
This commit is contained in:
Jason Dove
2025-07-21 11:00:39 +00:00
committed by GitHub
parent 011f16da9f
commit 7a4c832156
17 changed files with 107 additions and 46 deletions
+2
View File
@@ -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 - `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 - `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 - 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 ### Fixed
- Fix QSV acceleration in docker with older Intel devices - Fix QSV acceleration in docker with older Intel devices
@@ -3,4 +3,4 @@
namespace ErsatzTV.Application.MediaCards; namespace ErsatzTV.Application.MediaCards;
public record ActorCardViewModel(int Id, string Name, string Role, string Thumb, MediaItemState State) : 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);
@@ -14,4 +14,5 @@ public record ArtistCardViewModel(
Subtitle, Subtitle,
SortTitle, SortTitle,
Poster, Poster,
State); State,
HasMediaInfo: false);
@@ -14,7 +14,8 @@ public record ImageCardViewModel(
Subtitle, Subtitle,
SortTitle, SortTitle,
Poster, Poster,
State) State,
HasMediaInfo: true)
{ {
public int CustomIndex { get; set; } public int CustomIndex { get; set; }
} }
@@ -8,4 +8,5 @@ public record MediaCardViewModel(
string Subtitle, string Subtitle,
string SortTitle, string SortTitle,
string Poster, string Poster,
MediaItemState State); MediaItemState State,
bool HasMediaInfo);
@@ -14,7 +14,8 @@ public record MovieCardViewModel(
Subtitle, Subtitle,
SortTitle, SortTitle,
Poster, Poster,
State) State,
HasMediaInfo: true)
{ {
public int CustomIndex { get; set; } public int CustomIndex { get; set; }
} }
@@ -18,7 +18,8 @@ public record MusicVideoCardViewModel(
Subtitle, Subtitle,
SortTitle, SortTitle,
Poster, Poster,
State) State,
HasMediaInfo: true)
{ {
public int CustomIndex { get; set; } public int CustomIndex { get; set; }
} }
@@ -14,7 +14,8 @@ public record OtherVideoCardViewModel(
Subtitle, Subtitle,
SortTitle, SortTitle,
Poster, Poster,
State) State,
HasMediaInfo: true)
{ {
public int CustomIndex { get; set; } public int CustomIndex { get; set; }
} }
@@ -14,7 +14,8 @@ public record RemoteStreamCardViewModel(
Subtitle, Subtitle,
SortTitle, SortTitle,
Poster, Poster,
State) State,
HasMediaInfo: true)
{ {
public int CustomIndex { get; set; } public int CustomIndex { get; set; }
} }
@@ -14,7 +14,8 @@ public record SongCardViewModel(
Subtitle, Subtitle,
SortTitle, SortTitle,
Poster, Poster,
State) State,
HasMediaInfo: true)
{ {
public int CustomIndex { get; set; } public int CustomIndex { get; set; }
} }
@@ -24,4 +24,5 @@ public record TelevisionEpisodeCardViewModel(
$"Episode {Episode}", $"Episode {Episode}",
SortTitle, SortTitle,
Poster, Poster,
State); State,
HasMediaInfo: true);
@@ -17,4 +17,5 @@ public record TelevisionSeasonCardViewModel(
Subtitle, Subtitle,
SortTitle, SortTitle,
Poster, Poster,
State); State,
HasMediaInfo: false);
@@ -14,4 +14,5 @@ public record TelevisionShowCardViewModel(
Subtitle, Subtitle,
SortTitle, SortTitle,
Poster, Poster,
State); State,
HasMediaInfo: false);
@@ -14,4 +14,5 @@ public record MediaCollectionViewModel(
string.Empty, string.Empty,
Name, Name,
string.Empty, string.Empty,
State); State,
HasMediaInfo: false);
@@ -30,7 +30,7 @@ public class GetMediaItemInfoHandler : IRequestHandler<GetMediaItemInfo, Either<
.ThenInclude(l => l.MediaSource) .ThenInclude(l => l.MediaSource)
// TODO: support all media types here // TODO: support all media types here
.Include(i => (i as Movie).MovieMetadata) .Include(i => (i as Movie).MovieMetadata)
.ThenInclude(mv => mv.Subtitles) .ThenInclude(mm => mm.Subtitles)
.Include(i => (i as Movie).MediaVersions) .Include(i => (i as Movie).MediaVersions)
.ThenInclude(mv => mv.Chapters) .ThenInclude(mv => mv.Chapters)
.Include(i => (i as Movie).MediaVersions) .Include(i => (i as Movie).MediaVersions)
@@ -40,40 +40,42 @@ public class GetMediaItemInfoHandler : IRequestHandler<GetMediaItemInfo, Either<
.Include(i => (i as Episode).MediaVersions) .Include(i => (i as Episode).MediaVersions)
.ThenInclude(mv => mv.Streams) .ThenInclude(mv => mv.Streams)
.Include(i => (i as Episode).EpisodeMetadata) .Include(i => (i as Episode).EpisodeMetadata)
.ThenInclude(mv => mv.Subtitles) .ThenInclude(em => em.Subtitles)
.Include(i => (i as Episode).Season) .Include(i => (i as Episode).Season)
.ThenInclude(s => s.Show) .ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata) .ThenInclude(s => s.ShowMetadata)
.Include(i => (i as OtherVideo).OtherVideoMetadata) .Include(i => (i as OtherVideo).OtherVideoMetadata)
.ThenInclude(mv => mv.Subtitles) .ThenInclude(ovm => ovm.Subtitles)
.Include(i => (i as OtherVideo).MediaVersions) .Include(i => (i as OtherVideo).MediaVersions)
.ThenInclude(mv => mv.Chapters) .ThenInclude(mv => mv.Chapters)
.Include(i => (i as OtherVideo).MediaVersions) .Include(i => (i as OtherVideo).MediaVersions)
.ThenInclude(mv => mv.Streams) .ThenInclude(mv => mv.Streams)
.Include(i => (i as Image).ImageMetadata) .Include(i => (i as Image).ImageMetadata)
.ThenInclude(mv => mv.Subtitles) .ThenInclude(im => im.Subtitles)
.Include(i => (i as Image).MediaVersions) .Include(i => (i as Image).MediaVersions)
.ThenInclude(mv => mv.Chapters) .ThenInclude(mv => mv.Chapters)
.Include(i => (i as Image).MediaVersions) .Include(i => (i as Image).MediaVersions)
.ThenInclude(mv => mv.Streams) .ThenInclude(mv => mv.Streams)
.Include(i => (i as RemoteStream).RemoteStreamMetadata) .Include(i => (i as RemoteStream).RemoteStreamMetadata)
.ThenInclude(mv => mv.Subtitles) .ThenInclude(rsm => rsm.Subtitles)
.Include(i => (i as RemoteStream).MediaVersions) .Include(i => (i as RemoteStream).MediaVersions)
.ThenInclude(mv => mv.Chapters) .ThenInclude(mv => mv.Chapters)
.Include(i => (i as RemoteStream).MediaVersions) .Include(i => (i as RemoteStream).MediaVersions)
.ThenInclude(mv => mv.Streams) .ThenInclude(mv => mv.Streams)
.Include(i => (i as Song).SongMetadata) .Include(i => (i as Song).SongMetadata)
.ThenInclude(mv => mv.Subtitles) .ThenInclude(sm => sm.Subtitles)
.Include(i => (i as Song).MediaVersions) .Include(i => (i as Song).MediaVersions)
.ThenInclude(mv => mv.Chapters) .ThenInclude(mv => mv.Chapters)
.Include(i => (i as Song).MediaVersions) .Include(i => (i as Song).MediaVersions)
.ThenInclude(mv => mv.Streams) .ThenInclude(mv => mv.Streams)
.Include(i => (i as MusicVideo).MusicVideoMetadata) .Include(i => (i as MusicVideo).MusicVideoMetadata)
.ThenInclude(mv => mv.Subtitles) .ThenInclude(mvm => mvm.Subtitles)
.Include(i => (i as MusicVideo).MediaVersions) .Include(i => (i as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.Chapters) .ThenInclude(mv => mv.Chapters)
.Include(i => (i as MusicVideo).MediaVersions) .Include(i => (i as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.Streams) .ThenInclude(mv => mv.Streams)
.Include(i => (i as MusicVideo).Artist)
.ThenInclude(a => a.ArtistMetadata)
.SelectOneAsync(i => i.Id, i => i.Id == request.Id) .SelectOneAsync(i => i.Id, i => i.Id == request.Id)
.MapT(Project); .MapT(Project);
+67 -17
View File
@@ -1,6 +1,9 @@
@using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaItems
@using static Prelude @using static Prelude
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IDialogService Dialog
@inject IMediator Mediator
<div class="@((ContainerClass ?? "media-card-container mr-6") + " pb-3")" id="@($"item_{Data.MediaItemId}")"> <div class="@((ContainerClass ?? "media-card-container mr-6") + " pb-3")" id="@($"item_{Data.MediaItemId}")">
@if (SelectClicked.HasDelegate || !string.IsNullOrWhiteSpace(Href)) @if (SelectClicked.HasDelegate || !string.IsNullOrWhiteSpace(Href))
@@ -35,32 +38,53 @@
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning"/> <MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Warning"/>
</div> </div>
} }
<div class="media-card-overlay" style=""> <MudElement HtmlTag="div" Class="@_overlayClass" Style="">
<MudButton Style="height: 100%; width: 100%" <MudButton Style="height: 100%; width: 100%" OnClick="@OnOverlayClick" />
OnClick="OnOverlayClick">
</MudButton>
@if (SelectClicked.HasDelegate) @if (SelectClicked.HasDelegate)
{ {
<MudIconButton Color="@SelectColor" <MudIconButton Color="@SelectColor"
Icon="@(IsSelected ? Icons.Material.Filled.CheckBox : Icons.Material.Filled.CheckBoxOutlineBlank)" Icon="@(IsSelected ? Icons.Material.Filled.CheckBox : Icons.Material.Filled.CheckBoxOutlineBlank)"
Style="left: 0; position: absolute; top: 0;" Style="left: 0; position: absolute; top: 0;"
Class="media-card-select-checkbox"
OnClick="@(e => SelectClicked.InvokeAsync(e))"/> OnClick="@(e => SelectClicked.InvokeAsync(e))"/>
} }
@if (AddToCollectionClicked.HasDelegate && !IsSelectMode) @if (!IsSelectMode)
{ {
<MudIconButton Color="Color.Tertiary" @if (AddToCollectionClicked.HasDelegate || AddToPlaylistClicked.HasDelegate || Data.HasMediaInfo)
Icon="@Icons.Material.Filled.PlaylistAdd" {
Style="bottom: 0; left: 0; position: absolute;" <MudMenu Icon="@Icons.Material.Filled.MoreVert"
OnClick="@(() => AddToCollectionClicked.InvokeAsync(Data))"/> Style="bottom: 0; left: 0; position: absolute;"
OpenChanged="@OnMenuOpenChanged">
@if (AddToCollectionClicked.HasDelegate)
{
<MudMenuItem Icon="@Icons.Material.Filled.Add"
Label="Add To Collection"
OnClick="@(() => AddToCollectionClicked.InvokeAsync(Data))"/>
}
@if (AddToPlaylistClicked.HasDelegate)
{
<MudMenuItem Icon="@Icons.Material.Filled.PlaylistAdd"
Label="Add To Playlist"
OnClick="@(() => AddToPlaylistClicked.InvokeAsync(Data))"/>
}
@if (Data.HasMediaInfo)
{
<MudMenuItem Icon="@Icons.Material.Filled.Info"
Label="Show Media Info"
OnClick="@ShowMediaInfo"/>
}
</MudMenu>
}
@if (DeleteClicked.HasDelegate)
{
<MudIconButton Color="Color.Error"
Icon="@Icons.Material.Filled.Delete"
Style="bottom: 0; position: absolute; right: 0;"
OnClick="@(() => DeleteClicked.InvokeAsync(Data))"/>
}
} }
@if (DeleteClicked.HasDelegate && !IsSelectMode) </MudElement>
{
<MudIconButton Color="Color.Error"
Icon="@Icons.Material.Filled.Delete"
Style="bottom: 0; position: absolute; right: 0;"
OnClick="@(() => DeleteClicked.InvokeAsync(Data))"/>
}
</div>
</div> </div>
} }
else else
@@ -117,6 +141,9 @@
[Parameter] [Parameter]
public EventCallback<MediaCardViewModel> AddToCollectionClicked { get; set; } public EventCallback<MediaCardViewModel> AddToCollectionClicked { get; set; }
[Parameter]
public EventCallback<MediaCardViewModel> AddToPlaylistClicked { get; set; }
[Parameter] [Parameter]
public EventCallback<MouseEventArgs> SelectClicked { get; set; } public EventCallback<MouseEventArgs> SelectClicked { get; set; }
@@ -132,6 +159,8 @@
[Parameter] [Parameter]
public bool IsRemoteArtwork { get; set; } public bool IsRemoteArtwork { get; set; }
private string _overlayClass = "media-card-overlay";
private string GetPlaceholder(string sortTitle) private string GetPlaceholder(string sortTitle)
{ {
if (Placeholder != null) 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<BaseError, MediaItemInfo> 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<MediaItemInfoDialog>(Data.Title, parameters, options);
DialogResult _ = await dialog.Result;
}
}
} }
+4 -9
View File
@@ -31,15 +31,6 @@
.media-card-poster-placeholder { font-weight: bold; } .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 { .media-card-overlay {
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0.4);
border-radius: 4px; border-radius: 4px;
@@ -54,6 +45,10 @@
.media-card-overlay:hover { opacity: 1; } .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 .mud-input { height: 42px; }
.search-bar { .search-bar {