feat: add grid/list view toggle to track gallery with hover-reveal art cards
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
namespace DeepDrftPublic.Client.Controls;
|
||||
|
||||
public enum GalleryViewMode
|
||||
{
|
||||
Grid,
|
||||
List
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
@{
|
||||
var hasLink = !string.IsNullOrEmpty(TrackModel?.EntryKey);
|
||||
var trackHref = hasLink ? $"/track/{TrackModel!.EntryKey}" : null;
|
||||
var hasArt = !string.IsNullOrEmpty(TrackModel?.ImagePath);
|
||||
}
|
||||
|
||||
<div class="deepdrft-track-card-container">
|
||||
@if (ViewMode == GalleryViewMode.Grid)
|
||||
{
|
||||
<div class="deepdrft-track-card-container @(hasArt ? "deepdrft-track-card-container--art" : "")">
|
||||
|
||||
@* Cover and title/artist link to the detail page; the play button (below, outside any
|
||||
anchor) stays the sole playback entry point. display:contents keeps the grid intact. *@
|
||||
@@ -107,3 +110,102 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="deepdrft-track-row @(IsPlaying ? "deepdrft-track-row--playing" : "")">
|
||||
|
||||
<MudFab Color="Color.Tertiary"
|
||||
Size="Size.Medium"
|
||||
StartIcon="@PlayPauseIcon"
|
||||
OnClick="@PlayClick"
|
||||
Class="deepdrft-track-row-fab"/>
|
||||
|
||||
@if (hasLink)
|
||||
{
|
||||
<a href="@trackHref" class="deepdrft-track-row-link">
|
||||
@* art thumb *@
|
||||
@if (!string.IsNullOrEmpty(TrackModel?.ImagePath))
|
||||
{
|
||||
<div class="deepdrft-track-row-thumb"
|
||||
style="background-image: url('api/image/@Uri.EscapeDataString(TrackModel.ImagePath)');">
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="deepdrft-track-row-thumb deepdrft-track-row-thumb--fallback"></div>
|
||||
}
|
||||
|
||||
@* text block *@
|
||||
<div class="deepdrft-track-row-text">
|
||||
<MudText Typo="Typo.subtitle2" Class="deepdrft-track-title text-truncate">
|
||||
@TrackModel?.Artist
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Class="deepdrft-track-meta text-truncate">
|
||||
@TrackModel?.TrackName
|
||||
</MudText>
|
||||
</div>
|
||||
|
||||
@* right metadata *@
|
||||
<div class="deepdrft-track-row-meta">
|
||||
@if (!string.IsNullOrEmpty(TrackModel?.Genre))
|
||||
{
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Tertiary"
|
||||
Class="deepdrft-genre-chip">
|
||||
@TrackModel.Genre
|
||||
</MudChip>
|
||||
}
|
||||
@if (TrackModel?.ReleaseDate.HasValue == true)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="deepdrft-track-meta">
|
||||
@TrackModel.ReleaseDate.Value.Year
|
||||
</MudText>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* same structure without anchor *@
|
||||
@if (!string.IsNullOrEmpty(TrackModel?.ImagePath))
|
||||
{
|
||||
<div class="deepdrft-track-row-thumb"
|
||||
style="background-image: url('api/image/@Uri.EscapeDataString(TrackModel.ImagePath)');">
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="deepdrft-track-row-thumb deepdrft-track-row-thumb--fallback"></div>
|
||||
}
|
||||
<div class="deepdrft-track-row-text">
|
||||
<MudText Typo="Typo.subtitle2" Class="deepdrft-track-title text-truncate">
|
||||
@TrackModel?.Artist
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Class="deepdrft-track-meta text-truncate">
|
||||
@TrackModel?.TrackName
|
||||
</MudText>
|
||||
</div>
|
||||
<div class="deepdrft-track-row-meta">
|
||||
@if (!string.IsNullOrEmpty(TrackModel?.Genre))
|
||||
{
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Tertiary"
|
||||
Class="deepdrft-genre-chip">
|
||||
@TrackModel.Genre
|
||||
</MudChip>
|
||||
}
|
||||
@if (TrackModel?.ReleaseDate.HasValue == true)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="deepdrft-track-meta">
|
||||
@TrackModel.ReleaseDate.Value.Year
|
||||
</MudText>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public partial class TrackCard : ComponentBase
|
||||
[Parameter] public EventCallback<TrackDto> OnPause { get; set; }
|
||||
[Parameter] public bool IsPlaying { get; set; } = false;
|
||||
[Parameter] public bool IsPaused { get; set; } = false;
|
||||
[Parameter] public GalleryViewMode ViewMode { get; set; } = GalleryViewMode.Grid;
|
||||
|
||||
// Pause only when actively playing; every other state (idle, paused) reads as "press to play".
|
||||
private bool IsActivelyPlaying => IsPlaying && !IsPaused;
|
||||
|
||||
@@ -86,3 +86,96 @@
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mode A: hover-reveal overlay (art cards only) ──────────────────────── */
|
||||
|
||||
/* Gate the hidden-at-rest rule on (a) art present and (b) a hover-capable pointer.
|
||||
Fallback cards (no --art modifier) and touch devices always show the overlay. */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.deepdrft-track-card-container--art .deepdrft-track-card-content {
|
||||
opacity: 0;
|
||||
background: transparent;
|
||||
transition: opacity 180ms ease, background-color 180ms ease;
|
||||
}
|
||||
.deepdrft-track-card-container--art:hover .deepdrft-track-card-content {
|
||||
opacity: 1;
|
||||
background: var(--deepdrft-navy-mid, #162437);
|
||||
transition: opacity 180ms ease, background-color 180ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mode B: list row ───────────────────────────────────────────────────── */
|
||||
|
||||
.deepdrft-track-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
height: 80px;
|
||||
padding: 8px 16px;
|
||||
background: var(--deepdrft-navy-mid, #162437);
|
||||
border: 1px solid rgba(250, 250, 248, 0.12);
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.deepdrft-track-row-link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.deepdrft-track-row-fab {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.deepdrft-track-row-thumb {
|
||||
flex: 0 0 64px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.deepdrft-track-row-thumb--fallback {
|
||||
background: color-mix(in srgb, var(--deepdrft-navy-mid, #162437) 60%, rgba(250,250,248,0.1));
|
||||
border: 1px solid rgba(250, 250, 248, 0.12);
|
||||
}
|
||||
|
||||
.deepdrft-track-row-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.deepdrft-track-row-meta {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.deepdrft-track-row {
|
||||
height: auto;
|
||||
min-height: 72px;
|
||||
padding: 8px 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
.deepdrft-track-row-thumb {
|
||||
flex: 0 0 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="tracks-gallery-container">
|
||||
<MudGrid Spacing="6" Justify="Justify.Center">
|
||||
@foreach (var track in Tracks)
|
||||
{
|
||||
<MudItem xs="12" sm="6" md="4" lg="3" xl="3">
|
||||
<div class="deepdrft-track-gallery-item-center">
|
||||
<TrackCard TrackModel="@track"
|
||||
IsPlaying="@(IsPlaying && ActiveTrack?.Id == track.Id)"
|
||||
IsPaused="@(IsPaused && ActiveTrack?.Id == track.Id)"
|
||||
OnPlay="@HandlePlayClick"
|
||||
OnPause="@HandlePauseClick"/>
|
||||
</div>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
</MudContainer>
|
||||
@if (ViewMode == GalleryViewMode.Grid)
|
||||
{
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="tracks-gallery-container">
|
||||
<MudGrid Spacing="6" Justify="Justify.Center">
|
||||
@foreach (var track in Tracks)
|
||||
{
|
||||
<MudItem xs="12" sm="6" md="4" lg="3" xl="3">
|
||||
<div class="deepdrft-track-gallery-item-center">
|
||||
<TrackCard TrackModel="@track"
|
||||
ViewMode="@ViewMode"
|
||||
IsPlaying="@(IsPlaying && ActiveTrack?.Id == track.Id)"
|
||||
IsPaused="@(IsPaused && ActiveTrack?.Id == track.Id)"
|
||||
OnPlay="@HandlePlayClick"
|
||||
OnPause="@HandlePauseClick"/>
|
||||
</div>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
</MudContainer>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<div class="deepdrft-track-list">
|
||||
@foreach (var track in Tracks)
|
||||
{
|
||||
<TrackCard TrackModel="@track"
|
||||
ViewMode="@ViewMode"
|
||||
IsPlaying="@(IsPlaying && ActiveTrack?.Id == track.Id)"
|
||||
IsPaused="@(IsPaused && ActiveTrack?.Id == track.Id)"
|
||||
OnPlay="@HandlePlayClick"
|
||||
OnPause="@HandlePauseClick"/>
|
||||
}
|
||||
</div>
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public partial class TracksGallery : ComponentBase
|
||||
[Parameter] public TrackDto? ActiveTrack { get; set; }
|
||||
[Parameter] public bool IsPlaying { get; set; }
|
||||
[Parameter] public bool IsPaused { get; set; }
|
||||
[Parameter] public GalleryViewMode ViewMode { get; set; } = GalleryViewMode.Grid;
|
||||
|
||||
[Parameter] public EventCallback<TrackDto> OnPlay { get; set; }
|
||||
[Parameter] public EventCallback<TrackDto> OnPause { get; set; }
|
||||
|
||||
@@ -6,3 +6,10 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.deepdrft-track-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,19 @@
|
||||
<div class="tracks-view-container">
|
||||
@if (ViewModel.Page != null)
|
||||
{
|
||||
<div class="tracks-view-header">
|
||||
<MudToggleGroup T="GalleryViewMode" @bind-Value="_viewMode" Class="tracks-view-toggle">
|
||||
<MudToggleItem Value="GalleryViewMode.Grid">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ViewModule"/>
|
||||
</MudToggleItem>
|
||||
<MudToggleItem Value="GalleryViewMode.List">
|
||||
<MudIcon Icon="@Icons.Material.Filled.ViewList"/>
|
||||
</MudToggleItem>
|
||||
</MudToggleGroup>
|
||||
</div>
|
||||
<div class="tracks-content">
|
||||
<TracksGallery Tracks="@ViewModel.Page.Items"
|
||||
ViewMode="@_viewMode"
|
||||
ActiveTrack="@PlayerService.CurrentTrack"
|
||||
IsPlaying="@PlayerService.IsPlaying"
|
||||
IsPaused="@PlayerService.IsPaused"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using DeepDrftModels.DTOs;
|
||||
using DeepDrftPublic.Client.Controls;
|
||||
using DeepDrftPublic.Client.Services;
|
||||
using DeepDrftPublic.Client.ViewModels;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@@ -17,6 +18,9 @@ public partial class TracksView : ComponentBase, IDisposable
|
||||
private IStreamingPlayerService? _subscribedService;
|
||||
private PersistingComponentStateSubscription _persistingSubscription;
|
||||
|
||||
// Ephemeral view-mode selection — presentation-only, not persisted across navigation.
|
||||
private GalleryViewMode _viewMode = GalleryViewMode.Grid;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Carry the prerendered page across the prerender -> interactive (WASM) seam.
|
||||
|
||||
@@ -18,3 +18,9 @@
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.tracks-view-header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 0 12px 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user