Merge branch 'gallery-view-toggle' into dev
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 hasLink = !string.IsNullOrEmpty(TrackModel?.EntryKey);
|
||||||
var trackHref = hasLink ? $"/track/{TrackModel!.EntryKey}" : null;
|
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
|
@* 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. *@
|
anchor) stays the sole playback entry point. display:contents keeps the grid intact. *@
|
||||||
@@ -107,3 +110,102 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</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 EventCallback<TrackDto> OnPause { get; set; }
|
||||||
[Parameter] public bool IsPlaying { get; set; } = false;
|
[Parameter] public bool IsPlaying { get; set; } = false;
|
||||||
[Parameter] public bool IsPaused { 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".
|
// Pause only when actively playing; every other state (idle, paused) reads as "press to play".
|
||||||
private bool IsActivelyPlaying => IsPlaying && !IsPaused;
|
private bool IsActivelyPlaying => IsPlaying && !IsPaused;
|
||||||
|
|||||||
@@ -86,3 +86,100 @@
|
|||||||
height: 200px;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.deepdrft-track-row--playing {
|
||||||
|
border-left: 3px solid var(--deepdrft-green-interactive, #3aa163);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="tracks-gallery-container">
|
@if (ViewMode == GalleryViewMode.Grid)
|
||||||
|
{
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Large" Class="tracks-gallery-container">
|
||||||
<MudGrid Spacing="6" Justify="Justify.Center">
|
<MudGrid Spacing="6" Justify="Justify.Center">
|
||||||
@foreach (var track in Tracks)
|
@foreach (var track in Tracks)
|
||||||
{
|
{
|
||||||
<MudItem xs="12" sm="6" md="4" lg="3" xl="3">
|
<MudItem xs="12" sm="6" md="4" lg="3" xl="3">
|
||||||
<div class="deepdrft-track-gallery-item-center">
|
<div class="deepdrft-track-gallery-item-center">
|
||||||
<TrackCard TrackModel="@track"
|
<TrackCard TrackModel="@track"
|
||||||
|
ViewMode="@ViewMode"
|
||||||
IsPlaying="@(IsPlaying && ActiveTrack?.Id == track.Id)"
|
IsPlaying="@(IsPlaying && ActiveTrack?.Id == track.Id)"
|
||||||
IsPaused="@(IsPaused && ActiveTrack?.Id == track.Id)"
|
IsPaused="@(IsPaused && ActiveTrack?.Id == track.Id)"
|
||||||
OnPlay="@HandlePlayClick"
|
OnPlay="@HandlePlayClick"
|
||||||
@@ -13,4 +16,21 @@
|
|||||||
</MudItem>
|
</MudItem>
|
||||||
}
|
}
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudContainer>
|
</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 TrackDto? ActiveTrack { get; set; }
|
||||||
[Parameter] public bool IsPlaying { get; set; }
|
[Parameter] public bool IsPlaying { get; set; }
|
||||||
[Parameter] public bool IsPaused { 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> OnPlay { get; set; }
|
||||||
[Parameter] public EventCallback<TrackDto> OnPause { get; set; }
|
[Parameter] public EventCallback<TrackDto> OnPause { get; set; }
|
||||||
|
|||||||
@@ -6,3 +6,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.deepdrft-track-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,19 @@
|
|||||||
<div class="tracks-view-container">
|
<div class="tracks-view-container">
|
||||||
@if (ViewModel.Page != null)
|
@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">
|
<div class="tracks-content">
|
||||||
<TracksGallery Tracks="@ViewModel.Page.Items"
|
<TracksGallery Tracks="@ViewModel.Page.Items"
|
||||||
|
ViewMode="@_viewMode"
|
||||||
ActiveTrack="@PlayerService.CurrentTrack"
|
ActiveTrack="@PlayerService.CurrentTrack"
|
||||||
IsPlaying="@PlayerService.IsPlaying"
|
IsPlaying="@PlayerService.IsPlaying"
|
||||||
IsPaused="@PlayerService.IsPaused"
|
IsPaused="@PlayerService.IsPaused"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using DeepDrftModels.DTOs;
|
using DeepDrftModels.DTOs;
|
||||||
|
using DeepDrftPublic.Client.Controls;
|
||||||
using DeepDrftPublic.Client.Services;
|
using DeepDrftPublic.Client.Services;
|
||||||
using DeepDrftPublic.Client.ViewModels;
|
using DeepDrftPublic.Client.ViewModels;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
@@ -17,6 +18,9 @@ public partial class TracksView : ComponentBase, IDisposable
|
|||||||
private IStreamingPlayerService? _subscribedService;
|
private IStreamingPlayerService? _subscribedService;
|
||||||
private PersistingComponentStateSubscription _persistingSubscription;
|
private PersistingComponentStateSubscription _persistingSubscription;
|
||||||
|
|
||||||
|
// Ephemeral view-mode selection — presentation-only, not persisted across navigation.
|
||||||
|
private GalleryViewMode _viewMode = GalleryViewMode.Grid;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
// Carry the prerendered page across the prerender -> interactive (WASM) seam.
|
// Carry the prerendered page across the prerender -> interactive (WASM) seam.
|
||||||
|
|||||||
@@ -18,3 +18,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tracks-view-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user