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
@@ -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();