Merge branch 'player-desktop-redesign' into dev

This commit is contained in:
daniel-c-harvey
2026-06-04 19:34:27 -04:00
16 changed files with 182 additions and 230 deletions
@@ -1,63 +1,42 @@
@if (_isMinimized) @if (_isMinimized)
{ {
<div class="minimized-dock d-flex align-center justify-center" <MudFab Color="Color.Primary"
@onclick="@ToggleMinimized"> StartIcon="@Icons.Material.Filled.ExpandLess"
<MudIconButton Icon="@Icons.Material.Filled.ExpandLess" Size="Size.Large"
Color="Color.Primary" Class="minimized-dock"
Size="Size.Large" OnClick="@ToggleMinimized"/>
Class="minimized-button"
OnClick="@ToggleMinimized"/>
</div>
} }
else else
{ {
<div class="player-outer-container d-flex flex-column"> <div class="player-dock d-flex flex-column">
<MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container"> <MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container">
<div class="player-backdrop pa-3"> <MudPaper Elevation="8" Class="player-surface pa-3">
@if (_isDesktop) @if (_isDesktop)
{ {
@* Desktop Layout *@ @* Desktop Layout *@
<div class="d-flex align-center gap-3"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3" Class="player-row">
<div class="controls-left d-flex flex-column align-center gap-2"> <PlayerTransportZone IsPlaying="IsPlaying"
<div class="d-flex align-center gap-1"> IsLoaded="IsLoaded"
<PlayerControls IsPlaying="IsPlaying" IsLoading="IsLoading"
IsLoaded="IsLoaded" IsStreaming="IsStreaming"
TogglePlayPause="@TogglePlayPause" LoadProgress="LoadProgress"
Stop="@Stop"/> DisplayTime="DisplayTime"
@if (IsLoading && !IsStreaming) Duration="Duration"
{ TogglePlayPause="@TogglePlayPause"
<MudProgressCircular Color="Color.Tertiary" Stop="@Stop"
Size="Size.Small" Class="controls-left"/>
Max="1D"
Value="@LoadProgress"
Indeterminate="@(LoadProgress == 0)"/>
}
</div>
<TimestampLabel CurrentTime="DisplayTime" Duration="Duration"/>
</div>
<div class="d-flex flex-column flex-grow-1"> <PlayerSeekZone DisplayTime="DisplayTime"
<div class="seekbar-flex mx-3" Duration="Duration"
@onpointerdown="OnSeekStart" CanSeek="CanSeek"
@onpointerup="@(() => OnSeekEnd(_seekPosition))" OnSeekStart="@OnSeekStart"
@onpointerleave="@(async () => { if (_isSeeking) await OnSeekEnd(_seekPosition); })"> OnSeekEnd="@OnSeekEnd"
<MudSlider T="double" OnSeekChange="@OnSeekChange"
Min="0" Class="flex-grow-1"/>
Max="@(Duration ?? 0D)"
Step="0.1"
Value="@DisplayTime"
ValueChanged="@OnSeekChange"
Immediate="true"
Disabled="@(!CanSeek)"/>
</div>
<SpectrumVisualizer />
</div>
<div class="volume-right"> <VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/> </MudStack>
</div>
</div>
} }
else else
{ {
@@ -100,18 +79,9 @@ else
</div> </div>
} }
@* Control Buttons - positioned absolutely like original *@ @* Minimize / close — positioned absolutely top-right *@
<div class="player-controls d-flex align-center justify-center gap-1"> <PlayerWindowControls OnMinimize="@ToggleMinimized" OnClose="@Close"/>
<MudIconButton Icon="@Icons.Material.Filled.Minimize" </MudPaper>
Color="Color.Secondary"
Size="Size.Small"
OnClick="@ToggleMinimized"/>
<MudIconButton Icon="@Icons.Material.Filled.Close"
Color="Color.Secondary"
Size="Size.Small"
OnClick="@Close"/>
</div>
</div>
</MudContainer> </MudContainer>
@if (!string.IsNullOrEmpty(ErrorMessage)) @if (!string.IsNullOrEmpty(ErrorMessage))
@@ -1,7 +1,8 @@
/* Preserve key visual styles while simplifying layout */ /* Geometry, positioning, and animation only.
Colour, surface, and elevation come from MudBlazor theme props. */
/* Player outer container - fixed positioning */ /* Fixed dock to the viewport bottom */
.player-outer-container { .player-dock {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
@@ -11,115 +12,39 @@
margin: 0; margin: 0;
} }
/* Player inner container */
.player-inner-container { .player-inner-container {
padding: 1rem; padding: 1rem;
padding-bottom: 1.5rem; padding-bottom: 1.5rem;
} }
/* Custom backdrop blur container */ /* The visible surface is a MudPaper; scoped CSS only sets geometry + a hairline accent */
.player-backdrop { .player-surface {
position: relative; position: relative;
background: var(--deepdrft-theme-background-gray); border-radius: 16px;
backdrop-filter: blur(15px); border: 1px solid var(--mud-palette-primary);
-webkit-backdrop-filter: blur(15px);
border-radius: 1rem;
border: 2px solid var(--deepdrft-theme-primary);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
color: inherit;
transition: all 0.3s ease;
overflow: hidden; overflow: hidden;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
/* Charleston (Light Mode) - Iron frame effect */ /* Minimize / close cluster, pinned top-right of the surface */
:global(.deepdrft-theme-light) .player-backdrop { .player-surface ::deep .player-window-controls {
background: color-mix(in srgb, var(--charleston-cream) 92%, transparent);
border: 2px solid var(--charleston-iron);
box-shadow: 0 4px 20px color-mix(in srgb, var(--charleston-iron) 20%, transparent),
inset 0 0 0 1px color-mix(in srgb, var(--charleston-iron) 5%, transparent);
color: var(--charleston-iron);
}
/* Lowcountry (Dark Mode) - Warm sunset glow effect */
:global(.deepdrft-theme-dark) .player-backdrop {
background: color-mix(in srgb, var(--lowcountry-night) 88%, transparent);
border: 1px solid color-mix(in srgb, var(--lowcountry-coral) 50%, transparent);
box-shadow: 0 0 20px color-mix(in srgb, var(--lowcountry-coral) 25%, transparent),
0 0 40px color-mix(in srgb, var(--lowcountry-twilight) 15%, transparent),
0 4px 20px rgba(0, 0, 0, 0.4);
color: var(--lowcountry-moonlight);
}
/* Control buttons positioning */
.player-controls {
position: absolute; position: absolute;
top: 0.5rem; top: 0.5rem;
right: 0.5rem; right: 0.5rem;
} }
/* Minimized floating dock with gradient */ /* Minimized floating dock — positioning + hover only; colour from MudFab */
.minimized-dock { .minimized-dock {
position: fixed; position: fixed;
bottom: 60px; bottom: 60px;
right: 60px; right: 60px;
z-index: 1300; z-index: 1300;
width: 60px;
height: 60px;
border-radius: 50%;
cursor: pointer;
background: linear-gradient(135deg,
var(--deepdrft-theme-primary) 0%,
var(--deepdrft-theme-secondary) 50%,
var(--deepdrft-theme-tertiary) 100%
);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border: 2px solid var(--deepdrft-theme-secondary);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
/* Charleston (Light Mode) - Iron dock */
:global(.deepdrft-theme-light) .minimized-dock {
background: linear-gradient(135deg, var(--charleston-iron) 0%, var(--charleston-rose) 50%, var(--charleston-gold) 100%);
border: 2px solid var(--charleston-iron);
box-shadow: 0 4px 15px color-mix(in srgb, var(--charleston-iron) 40%, transparent);
}
/* Lowcountry (Dark Mode) - Warm sunset dock */
:global(.deepdrft-theme-dark) .minimized-dock {
background: linear-gradient(135deg, var(--lowcountry-coral) 0%, var(--lowcountry-twilight) 50%, var(--lowcountry-gold) 100%);
border: 2px solid color-mix(in srgb, var(--lowcountry-coral) 60%, transparent);
box-shadow: 0 0 20px color-mix(in srgb, var(--lowcountry-coral) 40%, transparent),
0 0 40px color-mix(in srgb, var(--lowcountry-twilight) 20%, transparent);
} }
.minimized-dock:hover { .minimized-dock:hover {
transform: scale(1.1); transform: scale(1.1);
} }
:global(.deepdrft-theme-light) .minimized-dock:hover {
box-shadow: 0 6px 20px color-mix(in srgb, var(--charleston-iron) 50%, transparent);
}
:global(.deepdrft-theme-dark) .minimized-dock:hover {
box-shadow: 0 0 30px color-mix(in srgb, var(--lowcountry-coral) 50%, transparent),
0 0 50px color-mix(in srgb, var(--lowcountry-twilight) 30%, transparent);
}
/* Minimized button styles */
.minimized-button {
border-radius: 50% !important;
background: transparent !important;
color: white !important;
transition: all 0.3s ease !important;
box-shadow: none !important;
border: none !important;
width: 48px !important;
height: 48px !important;
}
/* Spacer to prevent content overlap */ /* Spacer to prevent content overlap */
.player-spacer { .player-spacer {
height: 140px; height: 140px;
@@ -127,37 +52,10 @@
flex-shrink: 0; flex-shrink: 0;
} }
/* Essential layout adjustments only */
.controls-left {
min-width: 200px;
}
.seekbar-visualizer-container {
flex: 1;
display: flex;
flex-direction: column;
}
.seekbar-flex {
flex: 1;
}
.volume-right {
/*min-width: 140px;*/
}
/* Mobile responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.minimized-dock { .minimized-dock {
bottom: 15px; bottom: 15px;
right: 15px; right: 15px;
width: 56px;
height: 56px;
}
.minimized-button {
width: 44px !important;
height: 44px !important;
} }
.player-inner-container { .player-inner-container {
@@ -165,8 +63,7 @@
padding-bottom: 1.25rem; padding-bottom: 1.25rem;
} }
.player-backdrop { .player-surface {
border-radius: 1rem;
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
@@ -1,4 +1,4 @@
<div class="player-buttons"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIconButton Icon="@GetPlayIcon()" <MudIconButton Icon="@GetPlayIcon()"
Color="Color.Primary" Color="Color.Primary"
Size="Size.Large" Size="Size.Large"
@@ -9,4 +9,4 @@
Size="Size.Large" Size="Size.Large"
OnClick="@Stop" OnClick="@Stop"
Disabled="!IsLoaded"/> Disabled="!IsLoaded"/>
</div> </MudStack>
@@ -1,8 +0,0 @@
/* PlayerControls Component Styles */
/* Button spacing and alignment */
.player-buttons {
display: flex;
align-items: center;
gap: 0.5rem;
}
@@ -0,0 +1,18 @@
@namespace DeepDrftPublic.Client.Controls.AudioPlayerBar
<MudStack Row="false" Spacing="0" Class="@Class">
<div class="mx-3"
@onpointerdown="HandlePointerDown"
@onpointerup="HandlePointerUp"
@onpointerleave="HandlePointerLeave">
<MudSlider T="double"
Min="0"
Max="@(Duration ?? 0D)"
Step="0.1"
Value="@DisplayTime"
ValueChanged="HandleValueChanged"
Immediate="true"
Disabled="@(!CanSeek)"/>
</div>
<SpectrumVisualizer/>
</MudStack>
@@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Components;
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
/// <summary>
/// Centre zone of the player: seek slider over the spectrum visualizer.
/// Owns the pointer-gesture seek logic (drag-to-seek) in one place so it is no
/// longer duplicated inline between the desktop and mobile branches of the parent.
/// </summary>
public partial class PlayerSeekZone : ComponentBase
{
private double _seekPosition;
[Parameter] public double DisplayTime { get; set; }
[Parameter] public double? Duration { get; set; }
[Parameter] public bool CanSeek { get; set; }
[Parameter] public EventCallback OnSeekStart { get; set; }
[Parameter] public EventCallback<double> OnSeekEnd { get; set; }
[Parameter] public EventCallback<double> OnSeekChange { get; set; }
[Parameter] public string? Class { get; set; }
private async Task HandlePointerDown()
{
_seekPosition = DisplayTime;
await OnSeekStart.InvokeAsync();
}
private async Task HandlePointerUp()
{
await OnSeekEnd.InvokeAsync(_seekPosition);
}
private async Task HandlePointerLeave()
{
await OnSeekEnd.InvokeAsync(_seekPosition);
}
private async Task HandleValueChanged(double value)
{
_seekPosition = value;
await OnSeekChange.InvokeAsync(value);
}
}
@@ -0,0 +1,19 @@
@namespace DeepDrftPublic.Client.Controls.AudioPlayerBar
<MudStack Row="false" AlignItems="AlignItems.Center" Spacing="2" Class="@Class">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<PlayerControls IsPlaying="IsPlaying"
IsLoaded="IsLoaded"
TogglePlayPause="TogglePlayPause"
Stop="Stop"/>
@if (IsLoading && !IsStreaming)
{
<MudProgressCircular Color="Color.Tertiary"
Size="Size.Small"
Max="1D"
Value="@LoadProgress"
Indeterminate="@(LoadProgress == 0)"/>
}
</MudStack>
<TimestampLabel CurrentTime="DisplayTime" Duration="Duration"/>
</MudStack>
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Components;
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
public partial class PlayerTransportZone : ComponentBase
{
[Parameter] public bool IsPlaying { get; set; }
[Parameter] public bool IsLoaded { get; set; }
[Parameter] public bool IsLoading { get; set; }
[Parameter] public bool IsStreaming { get; set; }
[Parameter] public double LoadProgress { get; set; }
[Parameter] public double DisplayTime { get; set; }
[Parameter] public double? Duration { get; set; }
[Parameter] public EventCallback TogglePlayPause { get; set; }
[Parameter] public EventCallback Stop { get; set; }
[Parameter] public string? Class { get; set; }
}
@@ -0,0 +1,4 @@
/* Stable minimum width so the transport cluster doesn't reflow */
.controls-left {
min-width: 200px;
}
@@ -0,0 +1,12 @@
@namespace DeepDrftPublic.Client.Controls.AudioPlayerBar
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="player-window-controls">
<MudIconButton Icon="@Icons.Material.Filled.Minimize"
Color="Color.Secondary"
Size="Size.Small"
OnClick="OnMinimize"/>
<MudIconButton Icon="@Icons.Material.Filled.Close"
Color="Color.Secondary"
Size="Size.Small"
OnClick="OnClose"/>
</MudStack>
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Components;
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
public partial class PlayerWindowControls : ComponentBase
{
[Parameter] public EventCallback OnMinimize { get; set; }
[Parameter] public EventCallback OnClose { get; set; }
}
@@ -26,22 +26,11 @@
min-width: 4px; min-width: 4px;
height: var(--bar-height, 2%); height: var(--bar-height, 2%);
min-height: 2px; min-height: 2px;
background: var(--deepdrft-theme-secondary); background: var(--mud-palette-primary);
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
transition: height 0.05s ease-out; transition: height 0.05s ease-out;
} }
/* Charleston (Light Mode) - Iron to gold colored bars */
:global(.deepdrft-theme-light) .spectrum-bar {
background: linear-gradient(to top, var(--charleston-iron) 0%, var(--charleston-rose) 50%, var(--charleston-gold) 100%);
}
/* Lowcountry (Dark Mode) - Coral to gold bars with warm glow */
:global(.deepdrft-theme-dark) .spectrum-bar {
background: linear-gradient(to top, var(--lowcountry-coral) 0%, var(--lowcountry-gold) 100%);
box-shadow: 0 0 4px color-mix(in srgb, var(--lowcountry-gold) 40%, transparent);
}
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.spectrum-container { .spectrum-container {
@@ -1,5 +1,5 @@
<div class="timestamp-display"> <div class="timestamp-display">
<MudText Typo="Typo.body2" Class="time-text"> <MudText Typo="Typo.caption" Class="time-text">
@FormatTime(CurrentTime) / @(Duration.HasValue ? FormatTime(Duration.Value) : "--:--") @FormatTime(CurrentTime) / @(Duration.HasValue ? FormatTime(Duration.Value) : "--:--")
</MudText> </MudText>
</div> </div>
@@ -1,12 +1,5 @@
/* TimestampLabel Component Styles */ /* Layout stability so the timestamp doesn't reflow as digits change */
/* Timestamp display */
.timestamp-display { .timestamp-display {
min-width: 120px; min-width: 120px;
text-align: center; text-align: center;
} }
/* Time text styling */
.time-text {
font-family: monospace;
}
@@ -1,5 +1,5 @@
<div class="volume-controls"> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="volume-controls">
<MudIcon Icon="@GetVolumeIcon()" Class="volume-icon"/> <MudIcon Icon="@GetVolumeIcon()"/>
<MudSlider T="double" <MudSlider T="double"
Min="0" Min="0"
Max="1" Max="1"
@@ -7,4 +7,4 @@
Value="@Volume" Value="@Volume"
ValueChanged="@VolumeChanged" ValueChanged="@VolumeChanged"
Class="volume-slider"/> Class="volume-slider"/>
</div> </MudStack>
@@ -1,19 +1,8 @@
/* VolumeControls Component Styles */ /* Width caps only — layout/colour come from MudStack + theme */
/* Volume control container */
.volume-controls { .volume-controls {
display: flex;
align-items: center;
gap: 0.25rem;
width: 140px; width: 140px;
} }
/* Volume icon styling */
.volume-icon {
margin-right: 4px;
}
/* Volume slider styling */
.volume-slider { .volume-slider {
width: 100px; width: 100px;
} }