Player Client and Visual Enhancements

- Redesigned audio player bar to be mobile-friendly
 - Added unloading for track switching (needs to be fixed)
 - Added IsLoading status so loading spinner isn't hanging around when it shouldn't be
 - Normalized styles with scoped files (will further reduce)
 - Layout Cleanup
 - EF fixes (migrations now function for deployment)
 - deploy script updates (new dedicated host)
This commit is contained in:
daniel-c-harvey
2025-09-12 20:37:17 -04:00
parent 73d4b0a9c5
commit 9ac2c9182a
31 changed files with 763 additions and 179 deletions
@@ -1,87 +0,0 @@
@if (_isMinimized)
{
<div class="deepdrft-minimized-player-dock">
<MudIconButton Icon="@GetPlayIcon()"
Color="Color.Primary"
Size="Size.Large"
Class="deepdrft-minimized-player-button"
OnClick="@ToggleMinimized" />
</div>
}
else
{
@* Full-width outer container *@
<div class="deepdrft-player-outer-container">
<MudContainer MaxWidth="MaxWidth.Large" Class="deepdrft-player-inner-container">
<div class="deepdrft-audio-player-bar">
<div class="deepdrft-audio-player-content">
@* Controls section *@
<div class="deepdrft-audio-controls-section">
<div class="deepdrft-audio-buttons-row">
<MudIconButton Icon="@GetPlayIcon()"
Color="Color.Primary"
Size="Size.Large"
OnClick="@TogglePlayPause"
Disabled="!IsLoaded"/>
@if (IsLoaded)
{
<MudIconButton Icon="@Icons.Material.Filled.Stop"
Color="Color.Primary"
OnClick="@Stop"/>
}
</div>
<div class="deepdrft-audio-info-row">
<MudText Typo="Typo.body2" Class="font-monospace deepdrft-audio-time">
@FormatTime(CurrentTime) / @(Duration.HasValue ? FormatTime(Duration.Value) : "--:--")
</MudText>
@if (!IsLoaded)
{
<MudProgressCircular Color="Color.Tertiary" Value="@LoadProgress" Size="Size.Small"/>
}
</div>
</div>
@* Seek slider *@
<MudSlider T="double"
Min="0"
Max="@(Duration ?? 0D)"
Step="0.1"
Value="@CurrentTime"
ValueChanged="@OnSeek"
Disabled="!IsLoaded"
Class="deepdrft-audio-seek-slider"/>
@* Volume section *@
<div class="deepdrft-audio-volume-section">
<MudIcon Icon="@GetVolumeIcon()" Class="deepdrft-audio-volume-icon"/>
<MudSlider T="double"
Min="0"
Max="1"
Step="0.01"
Value="@Volume"
ValueChanged="@OnVolumeChange"
Class="deepdrft-audio-volume-slider"/>
</div>
<div class="deepdrft-audio-minimize-section">
<MudIconButton Icon="@Icons.Material.Filled.Minimize"
Color="Color.Secondary"
Size="Size.Small"
OnClick="@ToggleMinimized"/>
</div>
</div>
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<MudAlert Severity="Severity.Error" ShowCloseIcon="true" CloseIconClicked="ClearError" Class="ma-2">
@ErrorMessage
</MudAlert>
}
</div>
</MudContainer>
</div>
@* Spacer div to maintain layout spacing *@
<div class="deepdrft-player-spacer"></div>
}
@@ -0,0 +1,114 @@
@if (_isMinimized)
{
<div class="deepdrft-minimized-player-dock">
<MudIconButton Icon="@GetPlayIcon()"
Color="Color.Primary"
Size="Size.Large"
Class="deepdrft-minimized-player-button"
OnClick="@ToggleMinimized" />
</div>
}
else
{
@* Full-width outer container *@
<div class="deepdrft-player-outer-container">
<MudContainer MaxWidth="MaxWidth.Large" Class="deepdrft-player-inner-container">
<div class="deepdrft-audio-player-bar">
@* Full Screen *@
<div class="d-none d-md-block deepdrft-audio-player-content">
<div class="deepdrft-audio-controls-section">
<div class="deepdrft-audio-buttons-row">
<PlayerControls IsPlaying="IsPlaying"
IsLoaded="IsLoaded"
TogglePlayPause="@TogglePlayPause"
Stop="@Stop"/>
@if (!IsLoaded)
{
<MudProgressCircular Color="Color.Tertiary"
Max="1D"
Value="@LoadProgress"
Indeterminate="@(LoadProgress == 0)"/>
}
</div>
<TimestampLabel CurrentTime="CurrentTime"
Duration="Duration"/>
</div>
@* Seek slider *@
<MudSlider T="double"
Min="0"
Max="@(Duration ?? 0D)"
Step="0.1"
Value="@CurrentTime"
ValueChanged="@OnSeek"
Disabled="!IsLoaded"
Class="deepdrft-audio-seek-slider"/>
<div class="deepdrft-audio-volume-section">
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
</div>
</div>
@* Mobile *@
<div class="d-md-none deepdrft-audio-player-content">
<div class="deepdrft-audio-controls-section">
<div class="deepdrft-audio-buttons-row">
<PlayerControls IsPlaying="IsPlaying"
IsLoaded="IsLoaded"
TogglePlayPause="@TogglePlayPause"
Stop="@Stop"/>
<VolumeControls Volume="@Volume"
VolumeChanged="@OnVolumeChange"/>
@if (!IsLoaded)
{
<MudProgressCircular Color="Color.Tertiary"
Max="1D"
Value="@LoadProgress"
Indeterminate="@(LoadProgress == 0)"/>
}
</div>
<TimestampLabel CurrentTime="CurrentTime"
Duration="Duration"/>
</div>
@* Seek slider *@
<MudSlider T="double"
Min="0"
Max="@(Duration ?? 0D)"
Step="0.1"
Value="@CurrentTime"
ValueChanged="@OnSeek"
Disabled="!IsLoaded"
Class="deepdrft-audio-seek-slider"/>
</div>
<div class="deepdrft-audio-minimize-section">
<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>
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<MudAlert Severity="Severity.Error" ShowCloseIcon="true" CloseIconClicked="ClearError" Class="ma-2">
@ErrorMessage
</MudAlert>
}
</MudContainer>
</div>
@* Spacer div to maintain layout spacing *@
<div class="deepdrft-player-spacer"></div>
}
@@ -1,10 +1,8 @@
using DeepDrftModels.Entities;
using DeepDrftWeb.Client.Clients;
using DeepDrftWeb.Client.Services;
using Microsoft.AspNetCore.Components;
using DeepDrftWeb.Client.Services;
using MudBlazor;
namespace DeepDrftWeb.Client.Controls;
namespace DeepDrftWeb.Client.Controls.AudioPlayerBar;
public partial class AudioPlayerBar : ComponentBase
{
@@ -24,22 +22,18 @@ public partial class AudioPlayerBar : ComponentBase
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
PlayerService.OnStateChanged += StateHasChanged;
PlayerService.OnTrackSelected += Expand;
}
private string GetPlayIcon()
private async Task Expand()
{
return IsPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow;
if (_isMinimized)
{
_isMinimized = false;
StateHasChanged();
}
}
private string GetVolumeIcon()
{
if (Volume == 0) return Icons.Material.Filled.VolumeOff;
if (Volume < 0.5) return Icons.Material.Filled.VolumeDown;
return Icons.Material.Filled.VolumeUp;
}
private static string FormatTime(double seconds)
{
var timeSpan = TimeSpan.FromSeconds(seconds);
@@ -76,5 +70,23 @@ public partial class AudioPlayerBar : ComponentBase
_isMinimized = !_isMinimized;
StateHasChanged();
}
private async Task Close()
{
if (PlayerService.IsLoaded)
{
await PlayerService.Unload();
}
if (!_isMinimized)
{
_isMinimized = true;
StateHasChanged();
}
}
private string GetPlayIcon()
{
return IsPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow;
}
}
@@ -22,14 +22,15 @@
/* Player bar with rounded corners and semi-opaque background */
.deepdrft-audio-player-bar {
position: relative;
background: rgba(var(--mud-palette-surface-rgb), 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: var(--deepdrft-theme-background-gray);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border-radius: 1rem;
border: 1px solid rgba(var(--mud-palette-divider-rgb), 0.9);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
border: 1px solid var(--deepdrft-theme-primary);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4),
0 2px 10px var(--deepdrft-theme-secondary);
transition: all 0.3s ease;
color: var(--mud-palette-text-primary);
color: #ffffff;
overflow: hidden;
margin-bottom: 1rem;
}
@@ -54,13 +55,13 @@
justify-content: center;
align-items: center;
background: linear-gradient(135deg,
rgba(var(--mud-palette-primary-rgb), 0.95) 0%,
rgba(var(--mud-palette-secondary-rgb), 0.9) 50%,
rgba(var(--mud-palette-tertiary-rgb), 0.95) 100%
var(--deepdrft-theme-primary) 0%,
var(--deepdrft-theme-secondary) 50%,
var(--deepdrft-theme-tertiary) 100%
);
backdrop-filter: blur(15px);
border: 2px solid rgba(var(--mud-palette-secondary-rgb), 0.7);
box-shadow: 0 4px 20px rgba(var(--mud-palette-primary-rgb), 0.6),
border: 2px solid var(--deepdrft-theme-secondary);
box-shadow: 0 4px 20px var(--deepdrft-theme-primary),
0 2px 10px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
cursor: pointer;
@@ -109,6 +110,7 @@
align-items: center;
gap: 0.75rem;
margin-top: 0.25rem;
justify-items: center;
}
.deepdrft-audio-volume-section {
@@ -0,0 +1,103 @@
@if (_isMinimized)
{
<div class="minimized-dock d-flex align-center justify-center"
@onclick="@ToggleMinimized">
<MudIconButton Icon="@GetPlayIcon()"
Color="Color.Primary"
Size="Size.Large"
Class="minimized-button"
OnClick="@ToggleMinimized"/>
</div>
}
else
{
<div class="player-outer-container d-flex flex-column">
<MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container">
<div class="player-backdrop pa-3">
@* Desktop Layout *@
<div class="d-none d-md-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"/>
</div>
<TimestampLabel CurrentTime="CurrentTime" Duration="Duration"/>
</div>
<div class="seekbar-flex mx-3">
<MudSlider T="double"
Min="0"
Max="@(Duration ?? 0D)"
Step="0.1"
Value="@CurrentTime"
ValueChanged="@OnSeek"
Disabled="!IsLoaded"/>
</div>
<div class="volume-right">
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
</div>
</div>
@* Mobile Layout *@
<div class="d-md-none">
<div class="d-flex align-center justify-space-between mb-3">
<div class="d-flex align-center gap-2">
<PlayerControls IsPlaying="IsPlaying"
IsLoaded="IsLoaded"
TogglePlayPause="@TogglePlayPause"
Stop="@Stop"/>
@if (IsLoading)
{
<MudProgressCircular Color="Color.Tertiary"
Size="Size.Small"
Max="1D"
Value="@LoadProgress"
Indeterminate="@(LoadProgress == 0)"/>
}
</div>
<TimestampLabel CurrentTime="CurrentTime" Duration="Duration"/>
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
</div>
<MudSlider T="double"
Min="0"
Max="@(Duration ?? 0D)"
Step="0.1"
Value="@CurrentTime"
ValueChanged="@OnSeek"
Disabled="!IsLoaded"/>
</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>
</MudContainer>
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<MudAlert Severity="Severity.Error"
ShowCloseIcon="true"
CloseIconClicked="ClearError"
Class="ma-2">
@ErrorMessage
</MudAlert>
}
</div>
@* Spacer to prevent content overlap *@
<div class="player-spacer"></div>
}
@@ -0,0 +1,93 @@
using DeepDrftWeb.Client.Services;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace DeepDrftWeb.Client.Controls.AudioPlayerBar;
public partial class AudioPlayerBar2 : ComponentBase
{
[CascadingParameter] public required IPlayerService PlayerService { get; set; }
[Parameter] public bool ShowLoadProgress { get; set; } = true;
private bool _isMinimized = true;
private bool IsLoaded => PlayerService.IsLoaded;
private bool IsLoading => PlayerService.IsLoading;
private bool IsPlaying => PlayerService.IsPlaying;
private bool IsPaused => PlayerService.IsPaused;
private double CurrentTime => PlayerService.CurrentTime;
private double? Duration => PlayerService.Duration;
private double Volume => PlayerService.Volume;
private double LoadProgress => PlayerService.LoadProgress;
private string? ErrorMessage => PlayerService.ErrorMessage;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
PlayerService.OnStateChanged += StateHasChanged;
PlayerService.OnTrackSelected += Expand;
}
private async Task Expand()
{
if (_isMinimized)
{
_isMinimized = false;
StateHasChanged();
}
}
private static string FormatTime(double seconds)
{
var timeSpan = TimeSpan.FromSeconds(seconds);
return timeSpan.ToString(timeSpan.TotalHours >= 1 ? @"h\:mm\:ss" : @"m\:ss");
}
private async Task TogglePlayPause()
{
await PlayerService.TogglePlayPause();
}
private async Task Stop()
{
await PlayerService.Stop();
}
private async Task OnSeek(double position)
{
await PlayerService.Seek(position);
}
private async Task OnVolumeChange(double volume)
{
await PlayerService.SetVolume(volume);
}
private void ClearError()
{
PlayerService.ClearError();
}
private void ToggleMinimized()
{
_isMinimized = !_isMinimized;
StateHasChanged();
}
private async Task Close()
{
if (PlayerService.IsLoaded)
{
await PlayerService.Unload();
}
if (!_isMinimized)
{
_isMinimized = true;
StateHasChanged();
}
}
private string GetPlayIcon()
{
return IsPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow;
}
}
@@ -0,0 +1,131 @@
/* Preserve key visual styles while simplifying layout */
/* Player outer container - fixed positioning */
.player-outer-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1200;
padding: 0;
margin: 0;
}
/* Player inner container */
.player-inner-container {
padding: 1rem;
padding-bottom: 1.5rem;
}
/* Custom backdrop blur container */
.player-backdrop {
position: relative;
background: var(--deepdrft-theme-background-gray);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border-radius: 1rem;
border: 1px solid var(--deepdrft-theme-primary);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4),
0 2px 10px var(--deepdrft-theme-secondary);
color: #ffffff;
transition: all 0.3s ease;
overflow: hidden;
margin-bottom: 1rem;
}
/* Control buttons positioning */
.player-controls {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
/* Minimized floating dock with gradient */
.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 var(--deepdrft-theme-primary),
0 2px 10px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.minimized-dock:hover {
transform: scale(1.1);
box-shadow: 0 6px 25px rgba(var(--deepdrft-theme-primary), 0.8),
0 3px 15px rgba(0, 0, 0, 0.4);
}
/* 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;
width: 100%;
flex-shrink: 0;
}
/* Essential layout adjustments only */
.controls-left {
min-width: 200px;
}
.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;
margin-bottom: 1.25rem;
}
.player-spacer {
height: 160px;
}
}
@@ -0,0 +1,12 @@
<div class="player-buttons">
<MudIconButton Icon="@GetPlayIcon()"
Color="Color.Primary"
Size="Size.Large"
OnClick="@TogglePlayPause"
Disabled="!IsLoaded"/>
<MudIconButton Icon="@Icons.Material.Filled.Stop"
Color="Color.Primary"
Size="Size.Large"
OnClick="@Stop"
Disabled="!IsLoaded"/>
</div>
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace DeepDrftWeb.Client.Controls.AudioPlayerBar;
public partial class PlayerControls : ComponentBase
{
[Parameter] public required bool IsPlaying { get; set; }
[Parameter] public required bool IsLoaded { get; set; }
[Parameter] public required EventCallback TogglePlayPause { get; set; }
[Parameter] public required EventCallback Stop { get; set; }
private string GetPlayIcon()
{
return IsPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow;
}
}
@@ -0,0 +1,8 @@
/* PlayerControls Component Styles */
/* Button spacing and alignment */
.player-buttons {
display: flex;
align-items: center;
gap: 0.5rem;
}
@@ -0,0 +1,5 @@
<div class="timestamp-display">
<MudText Typo="Typo.body2" Class="time-text">
@FormatTime(CurrentTime) / @(Duration.HasValue ? FormatTime(Duration.Value) : "--:--")
</MudText>
</div>
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Components;
namespace DeepDrftWeb.Client.Controls.AudioPlayerBar;
public partial class TimestampLabel : ComponentBase
{
[Parameter] public required double CurrentTime { get; set; }
[Parameter] public required double? Duration { get; set; }
private static string FormatTime(double seconds)
{
var timeSpan = TimeSpan.FromSeconds(seconds);
return timeSpan.ToString(timeSpan.TotalHours >= 1 ? @"h\:mm\:ss" : @"m\:ss");
}
}
@@ -0,0 +1,12 @@
/* TimestampLabel Component Styles */
/* Timestamp display */
.timestamp-display {
min-width: 120px;
text-align: center;
}
/* Time text styling */
.time-text {
font-family: monospace;
}
@@ -0,0 +1,10 @@
<div class="volume-controls">
<MudIcon Icon="@GetVolumeIcon()" Class="volume-icon"/>
<MudSlider T="double"
Min="0"
Max="1"
Step="0.01"
Value="@Volume"
ValueChanged="@VolumeChanged"
Class="volume-slider"/>
</div>
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace DeepDrftWeb.Client.Controls.AudioPlayerBar;
public partial class VolumeControls : ComponentBase
{
[Parameter] public required double Volume { get; set; }
[Parameter] public required EventCallback<double> VolumeChanged { get; set; }
private string GetVolumeIcon()
{
if (Volume == 0) return Icons.Material.Filled.VolumeOff;
if (Volume < 0.5) return Icons.Material.Filled.VolumeDown;
return Icons.Material.Filled.VolumeUp;
}
}
@@ -0,0 +1,19 @@
/* VolumeControls Component Styles */
/* Volume control container */
.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;
}
+2 -5
View File
@@ -1,4 +1,5 @@
@using DeepDrftWeb.Client.Controls
@using DeepDrftWeb.Client.Controls.AudioPlayerBar
@inherits LayoutComponentBase
<MudThemeProvider Theme="@_themeManager.Theme" IsDarkMode="_isDarkMode" />
@@ -7,8 +8,6 @@
<MudSnackbarProvider />
<MudLayout>
<AudioPlayerService>
@* <MudThemeManagerButton OnClick="@((e) => OpenThemeManager(true))" /> *@
@* <MudThemeManager Open="_themeManagerOpen" OpenChanged="OpenThemeManager" Theme="_themeManager" ThemeChanged="UpdateTheme" /> *@
<MudAppBar Elevation="_themeManager.AppBarElevation">
<MudAvatar Class="mr-2">
<MudImage Src="img/deepdrft-logo.jpg"></MudImage>
@@ -16,13 +15,12 @@
<NavMenu />
<MudSpacer/>
<MudIconButton Icon="@(DarkLightModeButtonIcon)" Color="Color.Inherit" OnClick="@DarkModeToggle"/>
@* <MudIconButton Icon="@Icons.Material.Filled.MoreVert" Color="Color.Inherit" Edge="Edge.End"/> *@
</MudAppBar>
<MudMainContent Class="pt-16 deepdrft-layout-with-overlay-player">
<MudContainer MaxWidth="MaxWidth.False" Class="pa-4">
@Body
</MudContainer>
<AudioPlayerBar />
<AudioPlayerBar2 />
</MudMainContent>
</AudioPlayerService>
</MudLayout>
@@ -37,7 +35,6 @@
@code {
private bool _drawerOpen = true;
private bool _isDarkMode = true;
// private MudTheme? _theme = null;
protected override void OnInitialized()
{
+10 -11
View File
@@ -1,5 +1,4 @@
@page "/tracks"
@rendermode @(new InteractiveAutoRenderMode(prerender: false))
@using DeepDrftWeb.Client.Controls
@@ -20,21 +19,21 @@
SelectedChanged="@SetPage"
BoundaryCount="2"
MiddleCount="3"/>
<div class="interactivity-test mt-4">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="TestInteractivity">
Test Interactivity (@_clickCount)
</MudButton>
<MudText Typo="Typo.body2" Class="mt-2">
Lifecycle Status: @_lifecycleStatus
</MudText>
</div>
</div>
}
else
{
<div class="tracks-content">
<MudSkeleton Height="95%" Class="pa-2 ma-6"/>
<MudGrid Spacing="3">
@foreach (var i in Enumerable.Range(0, 12))
{
<MudItem xs="12" sm="6" md="4" lg="3" xl="3">
<div class="deepdrft-track-gallery-item-center">
<MudSkeleton Width="240px" Height="240px" SkeletonType="SkeletonType.Rectangle"/>
</div>
</MudItem>
}
</MudGrid>
</div>
<div class="tracks-footer">
<MudSkeleton Height="60px" Width="240px" Class="justify-center"/>
+1 -1
View File
@@ -53,7 +53,7 @@ public partial class TracksView : ComponentBase
if (track is null)
{
await PlayerService.Stop();
await PlayerService.Unload();
}
else
{
@@ -56,6 +56,11 @@ public class AudioInteropService : IAsyncDisposable
return await InvokeJsAsync<AudioOperationResult>("DeepDrftAudio.stop", playerId);
}
public async Task<AudioOperationResult> UnloadAsync(string playerId)
{
return await InvokeJsAsync<AudioOperationResult>("DeepDrftAudio.unload", playerId);
}
public async Task<AudioOperationResult> SeekAsync(string playerId, double position)
{
return await InvokeJsAsync<AudioOperationResult>("DeepDrftAudio.seek", playerId, position);
@@ -7,6 +7,7 @@ namespace DeepDrftWeb.Client.Services;
public class AudioPlaybackEngine : IAsyncDisposable
{
public event Events.EventAsync<double>? OnProgressChanged;
public event Events.EventAsync<double>? OnLoadChanged;
public event Events.EventAsync? OnPlaybackEnded;
public required TrackMediaClient Client { get; set; }
@@ -15,6 +16,7 @@ public class AudioPlaybackEngine : IAsyncDisposable
public string PlayerId { get; private set; } = Guid.NewGuid().ToString();
public bool IsInitialized { get; private set; } = false;
public bool IsLoaded { get; private set; } = false;
public bool IsLoading { get; private set; } = false;
public bool IsPlaying { get; private set; } = false;
public bool IsPaused { get; private set; } = false;
public double CurrentTime { get; private set; } = 0;
@@ -51,13 +53,26 @@ public class AudioPlaybackEngine : IAsyncDisposable
public async Task LoadTrack(TrackEntity track)
{
if (IsLoaded) return;
TrackMediaResponse? audio = null;
try
{
// Immediately reset state to indicate loading has started
ErrorMessage = null;
LoadProgress = 0;
IsLoaded = false;
IsLoading = true;
Duration = null;
CurrentTime = 0;
// Trigger load event immediately to show loading state in UI
if (OnLoadChanged != null) await OnLoadChanged.Invoke(0);
if (IsPlaying || IsPaused)
{
// If we were playing/paused, unload the current track
await Unload();
}
AudioOperationResult? loadResult = await AudioInterop.InitializeBufferedPlayerAsync(PlayerId);
if (loadResult?.Success != true)
{
@@ -65,7 +80,7 @@ public class AudioPlaybackEngine : IAsyncDisposable
return;
}
var mediaResult = await Client.GetTrackMedia(track.EntryKey);
var mediaResult = await Client.GetTrackMedia(track.EntryKey);
if (!mediaResult.Success)
{
ErrorMessage = mediaResult.GetMessage();
@@ -77,9 +92,7 @@ public class AudioPlaybackEngine : IAsyncDisposable
ErrorMessage = "No audio returned from server";
return;
}
TrackMediaResponse audio = mediaResult.Value;
await StreamAndPlay(audio);
audio = mediaResult.Value;
}
catch (Exception ex)
{
@@ -87,6 +100,21 @@ public class AudioPlaybackEngine : IAsyncDisposable
LoadProgress = 0;
IsLoaded = false;
}
finally
{
IsLoading = false;
}
try
{
if (audio == null) return;
await StreamAndPlay(audio);
}
catch (Exception ex)
{
ErrorMessage = $"Error streaming audio: {ex.Message}";
}
}
private async Task StreamAndPlay(TrackMediaResponse audio)
@@ -124,6 +152,7 @@ public class AudioPlaybackEngine : IAsyncDisposable
if (audio.ContentLength > 0)
{
LoadProgress = Math.Min(1.0, (double)totalBytesRead / audio.ContentLength);
if (OnLoadChanged != null) await OnLoadChanged.Invoke(LoadProgress);
}
}
} while (currentBytes > 0);
@@ -140,6 +169,9 @@ public class AudioPlaybackEngine : IAsyncDisposable
LoadProgress = 1.0;
IsLoaded = true;
ErrorMessage = null;
// Trigger final load completion event
if (OnLoadChanged != null) await OnLoadChanged.Invoke(1.0);
}
catch (Exception ex)
{
@@ -217,6 +249,35 @@ public class AudioPlaybackEngine : IAsyncDisposable
}
}
public async Task Unload()
{
if (!IsLoaded) return;
try
{
await Stop();
var result = await AudioInterop.UnloadAsync(PlayerId);
if (result.Success)
{
IsPlaying = false;
IsPaused = false;
CurrentTime = 0;
Duration = null;
LoadProgress = 0;
IsLoaded = false;
ErrorMessage = null;
}
else
{
ErrorMessage = $"Unload error: {result.Error}";
}
}
catch (Exception ex)
{
ErrorMessage = $"Error unloading track: {ex.Message}";
}
}
public async Task OnSeek(double position)
{
if (!IsLoaded) return;
@@ -1,4 +1,5 @@
using DeepDrftModels.Entities;
using NetBlocks.Models;
namespace DeepDrftWeb.Client.Services;
@@ -7,6 +8,7 @@ public interface IPlayerService
// State properties
bool IsInitialized { get; }
bool IsLoaded { get; }
bool IsLoading { get; }
bool IsPlaying { get; }
bool IsPaused { get; }
double CurrentTime { get; }
@@ -17,10 +19,12 @@ public interface IPlayerService
// Events for UI updates
event Action? OnStateChanged;
event Events.EventAsync OnTrackSelected;
// Control methods
Task SelectTrack(TrackEntity track);
Task Stop();
Task Unload();
Task TogglePlayPause();
Task Seek(double position);
Task SetVolume(double volume);
+19 -1
View File
@@ -1,4 +1,5 @@
using DeepDrftModels.Entities;
using NetBlocks.Models;
namespace DeepDrftWeb.Client.Services;
@@ -15,6 +16,7 @@ public class PlayerService : IPlayerService
// IPlayerService state properties with defensive checks
public bool IsInitialized => _isInitialized;
public bool IsLoaded => _isInitialized && _audioEngine?.IsLoaded == true;
public bool IsLoading => _isInitialized && _audioEngine?.IsLoading == true;
public bool IsPlaying => _isInitialized && _audioEngine?.IsPlaying == true;
public bool IsPaused => _isInitialized && _audioEngine?.IsPaused == true;
public double CurrentTime => _isInitialized ? _audioEngine?.CurrentTime ?? 0.0 : 0.0;
@@ -24,7 +26,8 @@ public class PlayerService : IPlayerService
public string? ErrorMessage => _isInitialized ? _audioEngine?.ErrorMessage : null;
public event Action? OnStateChanged;
public event Events.EventAsync? OnTrackSelected;
public async Task SelectTrack(TrackEntity track)
{
if (!_isInitialized)
@@ -32,9 +35,15 @@ public class PlayerService : IPlayerService
await EnsureInitializedAsync();
}
// Immediately notify UI that track selection is happening
OnStateChanged?.Invoke();
if (OnTrackSelected != null) await OnTrackSelected.Invoke();
if (_isInitialized && _audioEngine != null)
{
await _audioEngine.LoadTrack(track);
// Force a state change to ensure UI reflects final loaded state
OnStateChanged?.Invoke();
}
}
@@ -46,6 +55,14 @@ public class PlayerService : IPlayerService
await _audioEngine.Stop();
OnStateChanged?.Invoke();
}
public async Task Unload()
{
if (!_isInitialized || _audioEngine == null) return;
await _audioEngine.Unload();
OnStateChanged?.Invoke();
}
public async Task TogglePlayPause()
{
@@ -92,6 +109,7 @@ public class PlayerService : IPlayerService
// Wire up engine events to trigger state change notifications
_audioEngine.OnProgressChanged += async _ => OnStateChanged?.Invoke();
_audioEngine.OnPlaybackEnded += async () => OnStateChanged?.Invoke();
_audioEngine.OnLoadChanged += async _ => OnStateChanged?.Invoke();
_isInitialized = true;
OnStateChanged?.Invoke();