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)
{
<div class="minimized-dock d-flex align-center justify-center"
@onclick="@ToggleMinimized">
<MudIconButton Icon="@Icons.Material.Filled.ExpandLess"
Color="Color.Primary"
Size="Size.Large"
Class="minimized-button"
OnClick="@ToggleMinimized"/>
</div>
<MudFab Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ExpandLess"
Size="Size.Large"
Class="minimized-dock"
OnClick="@ToggleMinimized"/>
}
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">
<div class="player-backdrop pa-3">
<MudPaper Elevation="8" Class="player-surface pa-3">
@if (_isDesktop)
{
@* Desktop Layout *@
<div class="d-flex align-center gap-3">
<div class="controls-left d-flex flex-column align-center gap-2">
<div class="d-flex align-center gap-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)"/>
}
</div>
<TimestampLabel CurrentTime="DisplayTime" Duration="Duration"/>
</div>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3" Class="player-row">
<PlayerTransportZone IsPlaying="IsPlaying"
IsLoaded="IsLoaded"
IsLoading="IsLoading"
IsStreaming="IsStreaming"
LoadProgress="LoadProgress"
DisplayTime="DisplayTime"
Duration="Duration"
TogglePlayPause="@TogglePlayPause"
Stop="@Stop"
Class="controls-left"/>
<div class="d-flex flex-column flex-grow-1">
<div class="seekbar-flex mx-3"
@onpointerdown="OnSeekStart"
@onpointerup="@(() => OnSeekEnd(_seekPosition))"
@onpointerleave="@(async () => { if (_isSeeking) await OnSeekEnd(_seekPosition); })">
<MudSlider T="double"
Min="0"
Max="@(Duration ?? 0D)"
Step="0.1"
Value="@DisplayTime"
ValueChanged="@OnSeekChange"
Immediate="true"
Disabled="@(!CanSeek)"/>
</div>
<SpectrumVisualizer />
</div>
<PlayerSeekZone DisplayTime="DisplayTime"
Duration="Duration"
CanSeek="CanSeek"
OnSeekStart="@OnSeekStart"
OnSeekEnd="@OnSeekEnd"
OnSeekChange="@OnSeekChange"
Class="flex-grow-1"/>
<div class="volume-right">
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
</div>
</div>
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
</MudStack>
}
else
{
@@ -100,18 +79,9 @@ else
</div>
}
@* Control Buttons - positioned absolutely like original *@
<div class="player-controls d-flex align-center justify-center gap-1">
<MudIconButton Icon="@Icons.Material.Filled.Minimize"
Color="Color.Secondary"
Size="Size.Small"
OnClick="@ToggleMinimized"/>
<MudIconButton Icon="@Icons.Material.Filled.Close"
Color="Color.Secondary"
Size="Size.Small"
OnClick="@Close"/>
</div>
</div>
@* Minimize / close — positioned absolutely top-right *@
<PlayerWindowControls OnMinimize="@ToggleMinimized" OnClose="@Close"/>
</MudPaper>
</MudContainer>
@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 */
.player-outer-container {
/* Fixed dock to the viewport bottom */
.player-dock {
position: fixed;
bottom: 0;
left: 0;
@@ -11,115 +12,39 @@
margin: 0;
}
/* Player inner container */
.player-inner-container {
padding: 1rem;
padding-bottom: 1.5rem;
}
/* Custom backdrop blur container */
.player-backdrop {
/* The visible surface is a MudPaper; scoped CSS only sets geometry + a hairline accent */
.player-surface {
position: relative;
background: var(--deepdrft-theme-background-gray);
backdrop-filter: blur(15px);
-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;
border-radius: 16px;
border: 1px solid var(--mud-palette-primary);
overflow: hidden;
margin-bottom: 1rem;
}
/* Charleston (Light Mode) - Iron frame effect */
:global(.deepdrft-theme-light) .player-backdrop {
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 {
/* Minimize / close cluster, pinned top-right of the surface */
.player-surface ::deep .player-window-controls {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
/* Minimized floating dock with gradient */
/* Minimized floating dock — positioning + hover only; colour from MudFab */
.minimized-dock {
position: fixed;
bottom: 60px;
right: 60px;
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 {
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 */
.player-spacer {
height: 140px;
@@ -127,50 +52,22 @@
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) {
.minimized-dock {
bottom: 15px;
right: 15px;
width: 56px;
height: 56px;
}
.minimized-button {
width: 44px !important;
height: 44px !important;
}
.player-inner-container {
padding: 0.75rem;
padding-bottom: 1.25rem;
}
.player-backdrop {
border-radius: 1rem;
.player-surface {
margin-bottom: 1.25rem;
}
.player-spacer {
height: 160px;
}
}
}
@@ -1,4 +1,4 @@
<div class="player-buttons">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIconButton Icon="@GetPlayIcon()"
Color="Color.Primary"
Size="Size.Large"
@@ -9,4 +9,4 @@
Size="Size.Large"
OnClick="@Stop"
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;
height: var(--bar-height, 2%);
min-height: 2px;
background: var(--deepdrft-theme-secondary);
background: var(--mud-palette-primary);
border-radius: 2px 2px 0 0;
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 */
@media (max-width: 768px) {
.spectrum-container {
@@ -1,5 +1,5 @@
<div class="timestamp-display">
<MudText Typo="Typo.body2" Class="time-text">
<div class="timestamp-display">
<MudText Typo="Typo.caption" Class="time-text">
@FormatTime(CurrentTime) / @(Duration.HasValue ? FormatTime(Duration.Value) : "--:--")
</MudText>
</div>
</div>
@@ -1,12 +1,5 @@
/* TimestampLabel Component Styles */
/* Timestamp display */
/* Layout stability so the timestamp doesn't reflow as digits change */
.timestamp-display {
min-width: 120px;
text-align: center;
}
/* Time text styling */
.time-text {
font-family: monospace;
}
@@ -1,5 +1,5 @@
<div class="volume-controls">
<MudIcon Icon="@GetVolumeIcon()" Class="volume-icon"/>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="volume-controls">
<MudIcon Icon="@GetVolumeIcon()"/>
<MudSlider T="double"
Min="0"
Max="1"
@@ -7,4 +7,4 @@
Value="@Volume"
ValueChanged="@VolumeChanged"
Class="volume-slider"/>
</div>
</MudStack>
@@ -1,19 +1,8 @@
/* VolumeControls Component Styles */
/* Volume control container */
/* Width caps only — layout/colour come from MudStack + theme */
.volume-controls {
display: flex;
align-items: center;
gap: 0.25rem;
width: 140px;
}
/* Volume icon styling */
.volume-icon {
margin-right: 4px;
}
/* Volume slider styling */
.volume-slider {
width: 100px;
}
}