feature: Embed Frame Player
This commit is contained in:
@@ -9,12 +9,13 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="player-dock d-flex flex-column">
|
||||
<div class="@PlayerModeClass d-flex flex-column">
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container">
|
||||
<MudPaper Elevation="8" Class="player-surface pa-3">
|
||||
|
||||
<div class="player-layout">
|
||||
<PlayerTransportZone IsLoaded="IsLoaded"
|
||||
CanPlay="CanPlay"
|
||||
IsLoading="IsLoading"
|
||||
IsStreaming="IsStreaming"
|
||||
LoadProgress="LoadProgress"
|
||||
@@ -33,7 +34,10 @@ else
|
||||
</div>
|
||||
|
||||
@* Minimize / close — positioned absolutely top-right *@
|
||||
<PlayerWindowControls OnMinimize="@ToggleMinimized" OnClose="@Close"/>
|
||||
@if (!Fixed)
|
||||
{
|
||||
<PlayerWindowControls OnMinimize="@ToggleMinimized" OnClose="@Close"/>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudContainer>
|
||||
|
||||
@@ -48,6 +52,3 @@ else
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Spacer to prevent content overlap *@
|
||||
<div class="player-spacer"></div>
|
||||
@@ -7,7 +7,8 @@ namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
||||
public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
{
|
||||
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
||||
|
||||
[Parameter] public bool Fixed { get; set; } = false;
|
||||
|
||||
private bool _isMinimized = true;
|
||||
private bool _isSeeking = false;
|
||||
private double _seekPosition = 0;
|
||||
@@ -15,6 +16,15 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
|
||||
private bool IsLoaded => PlayerService?.IsLoaded ?? false;
|
||||
private bool IsLoading => PlayerService?.IsLoading ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// A track is staged when it has been selected as the current track but not yet loaded into
|
||||
/// the audio context (the embed's pre-gesture state). The first play click loads + plays it.
|
||||
/// </summary>
|
||||
private bool IsStaged => PlayerService is { IsLoaded: false, IsLoading: false, CurrentTrack: not null };
|
||||
|
||||
/// <summary>Play is available once a track is loaded, or staged and waiting for the first gesture.</summary>
|
||||
private bool CanPlay => IsLoaded || IsStaged;
|
||||
private bool IsStreaming => PlayerService?.CanStartStreaming ?? false;
|
||||
private bool IsStreamingMode => PlayerService?.IsStreamingMode ?? false;
|
||||
private double? Duration => PlayerService?.Duration;
|
||||
@@ -26,9 +36,15 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
/// Display time - shows seek position while dragging, otherwise current playback time.
|
||||
/// </summary>
|
||||
private double DisplayTime => _isSeeking ? _seekPosition : (PlayerService?.CurrentTime ?? 0);
|
||||
private string PlayerModeClass => Fixed ? "player-fixed" : "player-docked";
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (Fixed)
|
||||
{
|
||||
_isMinimized = false;
|
||||
}
|
||||
|
||||
// PlayerService is cascaded by AudioPlayerProvider; once it arrives,
|
||||
// wire our track-selection handler. The provider owns OnStateChanged —
|
||||
// we intentionally do NOT wrap or replace it. Because the cascade is
|
||||
@@ -60,6 +76,15 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
private async Task TogglePlayPause()
|
||||
{
|
||||
if (PlayerService == null) return;
|
||||
|
||||
// Gesture-gated start: a staged-but-unloaded track (the embed autoplay path) is loaded on
|
||||
// the first play click — the user gesture the browser requires before audio can start.
|
||||
if (IsStaged)
|
||||
{
|
||||
await PlayerService.SelectTrackStreaming(PlayerService.CurrentTrack!);
|
||||
return;
|
||||
}
|
||||
|
||||
await PlayerService.TogglePlayPause();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.player-fixed {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1200;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
::deep .player-inner-container {
|
||||
padding: 1rem;
|
||||
padding-bottom: 1.5rem;
|
||||
@@ -45,13 +55,6 @@
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Spacer to prevent content overlap */
|
||||
.player-spacer {
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
::deep .minimized-dock {
|
||||
bottom: 15px;
|
||||
@@ -66,10 +69,6 @@
|
||||
::deep .player-surface {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.player-spacer {
|
||||
height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Unified responsive player layout.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<PlayStateIcon Size="Size.Large"
|
||||
Color="Color.Primary"
|
||||
Disabled="!IsLoaded"
|
||||
Disabled="!CanPlay"
|
||||
OnToggle="@TogglePlayPause"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Stop"
|
||||
Color="Color.Primary"
|
||||
|
||||
@@ -5,6 +5,13 @@ namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
||||
public partial class PlayerControls : ComponentBase
|
||||
{
|
||||
[Parameter] public required bool IsLoaded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the play button is enabled. Distinct from <see cref="IsLoaded"/> so a staged-but-
|
||||
/// unloaded track (embed pre-gesture state) can still be played: the click loads it. Stop stays
|
||||
/// gated on <see cref="IsLoaded"/>.
|
||||
/// </summary>
|
||||
[Parameter] public bool CanPlay { get; set; }
|
||||
[Parameter] public required EventCallback TogglePlayPause { get; set; }
|
||||
[Parameter] public required EventCallback Stop { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="@Class">
|
||||
<PlayerControls IsLoaded="IsLoaded"
|
||||
CanPlay="CanPlay"
|
||||
TogglePlayPause="TogglePlayPause"
|
||||
Stop="Stop"/>
|
||||
@if (IsLoading && !IsStreaming)
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
||||
public partial class PlayerTransportZone : ComponentBase
|
||||
{
|
||||
[Parameter] public bool IsLoaded { get; set; }
|
||||
[Parameter] public bool CanPlay { get; set; }
|
||||
[Parameter] public bool IsLoading { get; set; }
|
||||
[Parameter] public bool IsStreaming { get; set; }
|
||||
[Parameter] public double LoadProgress { get; set; }
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
@namespace DeepDrftPublic.Client.Controls
|
||||
|
||||
<MudIconButton Icon="@Icon"
|
||||
Color="Color"
|
||||
Size="Size"
|
||||
Disabled="@Disabled"
|
||||
OnClick="@OnToggle"/>
|
||||
@if (!RendererInfo.IsInteractive)
|
||||
{
|
||||
@* Interactive runtime (WASM, or Server on first visit) not attached yet — the prerendered
|
||||
button has no wired click handler, so clicks would vanish. Show a spinner in its place
|
||||
until the component hydrates, at which point it re-renders into the live button. *@
|
||||
<MudProgressCircular Color="Color"
|
||||
Size="Size"
|
||||
Indeterminate="true"
|
||||
Class="mud-icon-button" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIconButton Icon="@Icon"
|
||||
Color="Color"
|
||||
Size="Size"
|
||||
Disabled="@Disabled"
|
||||
OnClick="@OnToggle" />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user