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"/> <VolumeZone Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
<PlayerSeekZone CurrentTrack="CurrentTrack" <div class="meta-zone">
OnSeekStart="@OnSeekStart" <TrackMetaLabel Track="CurrentTrack"/>
</div>
<PlayerSeekZone OnSeekStart="@OnSeekStart"
OnSeekEnd="@OnSeekEnd" OnSeekEnd="@OnSeekEnd"
OnSeekChange="@OnSeekChange" OnSeekChange="@OnSeekChange"
Class="seek-zone"/> Class="seek-zone"/>
@@ -38,7 +38,7 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
/// Display time - shows seek position while dragging, otherwise current playback time. /// Display time - shows seek position while dragging, otherwise current playback time.
/// </summary> /// </summary>
private double DisplayTime => _isSeeking ? _seekPosition : (PlayerService?.CurrentTime ?? 0); 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() protected override void OnParametersSet()
{ {
@@ -71,29 +71,53 @@
} }
} }
/* Unified responsive player layout. /* Unified responsive player layout — CSS Grid with named areas redefined per breakpoint.
Wide and narrow shapes are pure CSS — no runtime breakpoint subscription. The metadata (meta-zone) detaches from the waveform (seek-zone) in the mid band, which a
Children are targeted by their stable classes (transport-zone, volume-zone, flex order-swap can't express, so each of the four zones is placed by grid-area and the three
seek-zone) rather than positional nth-child, since all three render a .mud-stack shapes are pure template-area swaps — no runtime breakpoint subscription. min-width:0 on the
root and positional selectors would be fragile. */ shrinkable centre zones lets the title truncate and the waveform shrink instead of overflowing. */
.player-layout { .player-layout {
display: flex; display: grid;
flex-wrap: wrap;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding-right: 2.5rem; /* clear the abs-positioned PlayerWindowControls */ padding-right: 2.5rem; /* clear the abs-positioned PlayerWindowControls */
} }
/* Wide (>= 600px): single row, seek zone grows and sits between transport and volume */ ::deep .transport-zone { grid-area: transport; }
@media (min-width: 600px) { ::deep .meta-zone { grid-area: meta; min-width: 0; }
::deep .transport-zone { order: 1; } ::deep .seek-zone { grid-area: waveform; min-width: 0; }
::deep .seek-zone { order: 2; flex-grow: 1; flex-basis: 0; } ::deep .volume-zone { grid-area: volume; }
::deep .volume-zone { order: 3; flex-shrink: 0; }
/* 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 */ /* Mid (600900px): metadata rides the top row between transport and volume; the waveform gets
@media (max-width: 599.98px) { the whole bottom row to itself rather than being squeezed beside the metadata. */
::deep .transport-zone { order: 1; } @media (min-width: 600px) and (max-width: 899.98px) {
::deep .volume-zone { order: 2; } .player-layout {
::deep .seek-zone { flex-basis: 100%; order: 3; } 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" OnSeekChange="OnSeekChange"
OnSeekEnd="OnSeekEnd" OnSeekEnd="OnSeekEnd"
Class="seek-waveform"/> Class="seek-waveform"/>
<TrackMetaLabel Track="CurrentTrack"/>
</MudStack> </MudStack>
@@ -1,17 +1,16 @@
using DeepDrftModels.DTOs;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar; namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
/// <summary> /// <summary>
/// Centre zone of the player: the <see cref="WaveformSeeker"/> over the now-playing metadata row /// Centre zone of the player: the <see cref="WaveformSeeker"/>. The seeker owns the pointer-gesture
/// (<see cref="TrackMetaLabel"/>). The seeker owns the pointer-gesture seek logic and reads playback /// seek logic and reads playback state off the cascaded player service directly; this zone just
/// state off the cascaded player service directly; this zone just forwards the seek callbacks up to /// forwards the seek callbacks up to <see cref="AudioPlayerBar"/> (whose wiring is unchanged).
/// <see cref="AudioPlayerBar"/> (whose wiring is unchanged) and renders the current track's metadata. /// 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> /// </summary>
public partial class PlayerSeekZone : ComponentBase public partial class PlayerSeekZone : ComponentBase
{ {
[Parameter] public TrackDto? CurrentTrack { get; set; }
[Parameter] public EventCallback OnSeekStart { get; set; } [Parameter] public EventCallback OnSeekStart { get; set; }
[Parameter] public EventCallback<double> OnSeekEnd { get; set; } [Parameter] public EventCallback<double> OnSeekEnd { get; set; }
[Parameter] public EventCallback<double> OnSeekChange { get; set; } [Parameter] public EventCallback<double> OnSeekChange { get; set; }
@@ -7,7 +7,7 @@
<MudText Typo="Typo.subtitle2" Class="track-meta-title text-truncate"> <MudText Typo="Typo.subtitle2" Class="track-meta-title text-truncate">
@Track.TrackName @Track.TrackName
</MudText> </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"> <MudText Typo="Typo.caption" Class="track-meta-artist text-truncate">
@Track.Artist @Track.Artist
</MudText> </MudText>
@@ -40,3 +40,56 @@
::deep .track-meta-year { ::deep .track-meta-year {
opacity: 0.75; 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 @namespace DeepDrftPublic.Client.Controls.AudioPlayerBar
<MudStack Row="false" AlignItems="AlignItems.Center" Spacing="1" Class="@($"volume-zone {Class}".TrimEnd())"> <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"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="volume-row">
<MudIcon Icon="@GetVolumeIcon()"/> <MudIcon Icon="@GetVolumeIcon()"/>
<MudSlider T="double" <MudSlider T="double"
@@ -1,9 +1,9 @@
/* Width caps only — layout/colour come from MudStack + theme. /* Width caps only — layout/colour come from MudStack + theme.
The zone stacks the spectrum visualizer above the volume row. Width is sized The zone stacks the spectrum visualizer above the volume row, both sized to the same
so 24 spectrum bars (4px min + 2px gap ≈ 140px) render without clipping under compact width so the spectrum matches the slider. 75px fits 10 spectrum bars (4px min +
the container's overflow:hidden, while staying compact. */ 2px gap ≈ 70px) without clipping under the container's overflow:hidden. */
.volume-zone { .volume-zone {
width: 150px; width: 75px;
} }
.volume-row { .volume-row {