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:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user