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