Player Layout

This commit is contained in:
daniel-c-harvey
2026-06-06 17:28:39 -04:00
parent 3e4ddbb2a6
commit 6b18d7cc1e
9 changed files with 111 additions and 33 deletions
@@ -27,8 +27,11 @@ else
<VolumeZone Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
<PlayerSeekZone CurrentTrack="CurrentTrack"
OnSeekStart="@OnSeekStart"
<div class="meta-zone">
<TrackMetaLabel Track="CurrentTrack"/>
</div>
<PlayerSeekZone OnSeekStart="@OnSeekStart"
OnSeekEnd="@OnSeekEnd"
OnSeekChange="@OnSeekChange"
Class="seek-zone"/>
@@ -38,7 +38,7 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
/// Display time - shows seek position while dragging, otherwise current playback time.
/// </summary>
private double DisplayTime => _isSeeking ? _seekPosition : (PlayerService?.CurrentTime ?? 0);
private string PlayerModeClass => Fixed ? "player-fixed" : "player-docked";
private string PlayerModeClass => Fixed ? "player-fixed" : "player-dock";
protected override void OnParametersSet()
{
@@ -71,29 +71,53 @@
}
}
/* Unified responsive player layout.
Wide and narrow shapes are pure CSS — no runtime breakpoint subscription.
Children are targeted by their stable classes (transport-zone, volume-zone,
seek-zone) rather than positional nth-child, since all three render a .mud-stack
root and positional selectors would be fragile. */
/* Unified responsive player layout — CSS Grid with named areas redefined per breakpoint.
The metadata (meta-zone) detaches from the waveform (seek-zone) in the mid band, which a
flex order-swap can't express, so each of the four zones is placed by grid-area and the three
shapes are pure template-area swaps — no runtime breakpoint subscription. min-width:0 on the
shrinkable centre zones lets the title truncate and the waveform shrink instead of overflowing. */
.player-layout {
display: flex;
flex-wrap: wrap;
display: grid;
align-items: center;
gap: 8px;
padding-right: 2.5rem; /* clear the abs-positioned PlayerWindowControls */
}
/* Wide (>= 600px): single row, seek zone grows and sits between transport and volume */
@media (min-width: 600px) {
::deep .transport-zone { order: 1; }
::deep .seek-zone { order: 2; flex-grow: 1; flex-basis: 0; }
::deep .volume-zone { order: 3; flex-shrink: 0; }
::deep .transport-zone { grid-area: transport; }
::deep .meta-zone { grid-area: meta; min-width: 0; }
::deep .seek-zone { grid-area: waveform; min-width: 0; }
::deep .volume-zone { grid-area: volume; }
/* Wide (>= 900px): single row — transport and volume flank the centre column; the metadata
sits directly under the waveform (transport/volume span both rows, centred). */
@media (min-width: 900px) {
.player-layout {
grid-template-columns: auto minmax(360px, 1fr) auto;
grid-template-areas:
"transport waveform volume"
"transport meta volume";
}
}
/* Narrow (< 600px): transport + volume on top row, seek full-width below */
@media (max-width: 599.98px) {
::deep .transport-zone { order: 1; }
::deep .volume-zone { order: 2; }
::deep .seek-zone { flex-basis: 100%; order: 3; }
/* Mid (600900px): metadata rides the top row between transport and volume; the waveform gets
the whole bottom row to itself rather than being squeezed beside the metadata. */
@media (min-width: 600px) and (max-width: 899.98px) {
.player-layout {
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-areas:
"transport meta volume"
"waveform waveform waveform";
}
}
/* Narrow (< 600px): transport + volume share the top row; waveform then metadata stack full-width
below — the most compressed shape. */
@media (max-width: 599.98px) {
.player-layout {
grid-template-columns: auto 1fr auto;
grid-template-areas:
"transport . volume"
"waveform waveform waveform"
"meta meta meta";
}
}
@@ -5,5 +5,4 @@
OnSeekChange="OnSeekChange"
OnSeekEnd="OnSeekEnd"
Class="seek-waveform"/>
<TrackMetaLabel Track="CurrentTrack"/>
</MudStack>
@@ -1,17 +1,16 @@
using DeepDrftModels.DTOs;
using Microsoft.AspNetCore.Components;
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
/// <summary>
/// Centre zone of the player: the <see cref="WaveformSeeker"/> over the now-playing metadata row
/// (<see cref="TrackMetaLabel"/>). The seeker owns the pointer-gesture seek logic and reads playback
/// state off the cascaded player service directly; this zone just forwards the seek callbacks up to
/// <see cref="AudioPlayerBar"/> (whose wiring is unchanged) and renders the current track's metadata.
/// Centre zone of the player: the <see cref="WaveformSeeker"/>. The seeker owns the pointer-gesture
/// seek logic and reads playback state off the cascaded player service directly; this zone just
/// forwards the seek callbacks up to <see cref="AudioPlayerBar"/> (whose wiring is unchanged).
/// The now-playing metadata (<see cref="TrackMetaLabel"/>) is a sibling zone in the grid, not nested
/// here, so the responsive layouts can place it independently of the waveform.
/// </summary>
public partial class PlayerSeekZone : ComponentBase
{
[Parameter] public TrackDto? CurrentTrack { get; set; }
[Parameter] public EventCallback OnSeekStart { get; set; }
[Parameter] public EventCallback<double> OnSeekEnd { get; set; }
[Parameter] public EventCallback<double> OnSeekChange { get; set; }
@@ -7,7 +7,7 @@
<MudText Typo="Typo.subtitle2" Class="track-meta-title text-truncate">
@Track.TrackName
</MudText>
<MudText Typo="Typo.subtitle2"> - </MudText>
<MudText Typo="Typo.subtitle2" Class="track-meta-sep"> - </MudText>
<MudText Typo="Typo.caption" Class="track-meta-artist text-truncate">
@Track.Artist
</MudText>
@@ -40,3 +40,56 @@
::deep .track-meta-year {
opacity: 0.75;
}
/* The metadata's three shapes track the dock's layout bands (same breakpoints as the grid in
AudioPlayerBar.razor.css), not the label's own slot width — in the <600 band the slot is actually
full-width yet we still want it fully vertical, which a container query can't express.
Mid band (600900): 2×2 — title over artist on the left (start-justified), genre over year on the
right (end-justified). The row stays a row; only the two inner groups go vertical. */
@media (min-width: 600px) and (max-width: 899.98px) {
.track-meta-row {
align-items: flex-start;
}
.track-meta-identity {
flex-direction: column;
align-items: flex-start;
gap: 0;
}
.track-meta-accents {
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
::deep .track-meta-sep {
display: none;
}
}
/* Narrow band (<600): fully vertical — title / artist / genre / year all stacked, left-aligned. */
@media (max-width: 599.98px) {
.track-meta-row {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.track-meta-identity {
flex-direction: column;
align-items: flex-start;
gap: 0;
}
.track-meta-accents {
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
::deep .track-meta-sep {
display: none;
}
}
@@ -1,7 +1,7 @@
@namespace DeepDrftPublic.Client.Controls.AudioPlayerBar
<MudStack Row="false" AlignItems="AlignItems.Center" Spacing="1" Class="@($"volume-zone {Class}".TrimEnd())">
<SpectrumVisualizer BucketCount="24"/>
<SpectrumVisualizer BucketCount="10"/>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="volume-row">
<MudIcon Icon="@GetVolumeIcon()"/>
<MudSlider T="double"
@@ -1,9 +1,9 @@
/* Width caps only — layout/colour come from MudStack + theme.
The zone stacks the spectrum visualizer above the volume row. Width is sized
so 24 spectrum bars (4px min + 2px gap ≈ 140px) render without clipping under
the container's overflow:hidden, while staying compact. */
The zone stacks the spectrum visualizer above the volume row, both sized to the same
compact width so the spectrum matches the slider. 75px fits 10 spectrum bars (4px min +
2px gap ≈ 70px) without clipping under the container's overflow:hidden. */
.volume-zone {
width: 150px;
width: 75px;
}
.volume-row {