refactor(split): rename DeepDrftWeb -> DeepDrftPublic and DeepDrftWeb.Client -> DeepDrftPublic.Client (Phase 4)
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
@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">
|
||||
|
||||
@if (_isDesktop)
|
||||
{
|
||||
@* Desktop Layout *@
|
||||
<div class="d-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"/>
|
||||
@if (IsLoading && !IsStreaming)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Tertiary"
|
||||
Size="Size.Small"
|
||||
Max="1D"
|
||||
Value="@LoadProgress"
|
||||
Indeterminate="@(LoadProgress == 0)"/>
|
||||
}
|
||||
</div>
|
||||
<TimestampLabel CurrentTime="DisplayTime" Duration="Duration"/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<div class="seekbar-flex mx-3"
|
||||
@onpointerdown="OnSeekStart"
|
||||
@onpointerup="@(() => OnSeekEnd(_seekPosition))"
|
||||
@onpointerleave="@(() => { if (_isSeeking) OnSeekEnd(_seekPosition); })">
|
||||
<MudSlider T="double"
|
||||
Min="0"
|
||||
Max="@(Duration ?? 0D)"
|
||||
Step="0.1"
|
||||
Value="@DisplayTime"
|
||||
ValueChanged="@OnSeekChange"
|
||||
Immediate="true"
|
||||
Disabled="@(!CanSeek)"/>
|
||||
</div>
|
||||
<SpectrumVisualizer />
|
||||
</div>
|
||||
|
||||
<div class="volume-right">
|
||||
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* Mobile Layout *@
|
||||
<div>
|
||||
<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 && !IsStreaming)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Tertiary"
|
||||
Size="Size.Small"
|
||||
Max="1D"
|
||||
Value="@LoadProgress"
|
||||
Indeterminate="@(LoadProgress == 0)"/>
|
||||
}
|
||||
</div>
|
||||
<TimestampLabel CurrentTime="DisplayTime" Duration="Duration"/>
|
||||
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
<div @onpointerdown="OnSeekStart"
|
||||
@onpointerup="@(() => OnSeekEnd(_seekPosition))"
|
||||
@onpointerleave="@(() => { if (_isSeeking) OnSeekEnd(_seekPosition); })">
|
||||
<MudSlider T="double"
|
||||
Min="0"
|
||||
Max="@(Duration ?? 0D)"
|
||||
Step="0.1"
|
||||
Value="@DisplayTime"
|
||||
ValueChanged="@OnSeekChange"
|
||||
Immediate="true"
|
||||
Disabled="@(!CanSeek)"/>
|
||||
</div>
|
||||
<SpectrumVisualizer />
|
||||
</div>
|
||||
</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,166 @@
|
||||
using DeepDrftPublic.Client.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using MudBlazor.Services;
|
||||
|
||||
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
||||
|
||||
public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
{
|
||||
[CascadingParameter] public required IStreamingPlayerService PlayerService { get; set; }
|
||||
[Inject] public required IBrowserViewportService BrowserViewportService { get; set; }
|
||||
|
||||
private bool _isMinimized = true;
|
||||
private bool _isSeeking = false;
|
||||
private double _seekPosition = 0;
|
||||
private bool _isDesktop = true;
|
||||
private Guid _viewportSubscriptionId;
|
||||
|
||||
private bool IsLoaded => PlayerService.IsLoaded;
|
||||
private bool IsLoading => PlayerService.IsLoading;
|
||||
private bool IsStreaming => PlayerService.CanStartStreaming;
|
||||
private bool IsStreamingMode => PlayerService.IsStreamingMode;
|
||||
private bool IsPlaying => PlayerService.IsPlaying;
|
||||
private bool IsPaused => PlayerService.IsPaused;
|
||||
private double? Duration => PlayerService.Duration;
|
||||
private double Volume => PlayerService.Volume;
|
||||
private double LoadProgress => PlayerService.LoadProgress;
|
||||
private string? ErrorMessage => PlayerService.ErrorMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Display time - shows seek position while dragging, otherwise current playback time.
|
||||
/// </summary>
|
||||
private double DisplayTime => _isSeeking ? _seekPosition : PlayerService.CurrentTime;
|
||||
|
||||
/// <summary>
|
||||
/// Seek is enabled once track is loaded AND duration is known (from WAV header).
|
||||
/// This allows seeking even during streaming, including seeking beyond buffered content.
|
||||
/// </summary>
|
||||
private bool CanSeek => IsLoaded && Duration.HasValue && Duration.Value > 0;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
// Set up EventCallback for track selection
|
||||
PlayerService.OnTrackSelected = new EventCallback(this, Expand);
|
||||
|
||||
// Store the original OnStateChanged callback set by the provider
|
||||
var originalOnStateChanged = PlayerService.OnStateChanged;
|
||||
|
||||
// Set up a wrapper that calls both the original callback and our StateHasChanged
|
||||
PlayerService.OnStateChanged = new EventCallback(this, async () =>
|
||||
{
|
||||
// Invoke the original callback (AudioPlayerProvider's StateHasChanged)
|
||||
if (originalOnStateChanged.HasValue)
|
||||
{
|
||||
await originalOnStateChanged.Value.InvokeAsync();
|
||||
}
|
||||
// Also trigger our own re-render
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
|
||||
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 void OnSeekStart()
|
||||
{
|
||||
_isSeeking = true;
|
||||
_seekPosition = PlayerService.CurrentTime;
|
||||
}
|
||||
|
||||
private void OnSeekChange(double position)
|
||||
{
|
||||
_seekPosition = position;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task OnSeekEnd(double position)
|
||||
{
|
||||
_isSeeking = false;
|
||||
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;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
var breakpoint = await BrowserViewportService.GetCurrentBreakpointAsync();
|
||||
_isDesktop = breakpoint >= Breakpoint.Sm;
|
||||
|
||||
_viewportSubscriptionId = Guid.NewGuid();
|
||||
await BrowserViewportService.SubscribeAsync(
|
||||
_viewportSubscriptionId,
|
||||
args =>
|
||||
{
|
||||
_isDesktop = args.Breakpoint >= Breakpoint.Sm;
|
||||
InvokeAsync(StateHasChanged);
|
||||
},
|
||||
new ResizeOptions { NotifyOnBreakpointOnly = true },
|
||||
fireImmediately: true);
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await BrowserViewportService.UnsubscribeAsync(_viewportSubscriptionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/* 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: 2px solid var(--deepdrft-theme-primary);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
color: inherit;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Charleston (Light Mode) - Iron frame effect */
|
||||
:global(.deepdrft-theme-light) .player-backdrop {
|
||||
background: color-mix(in srgb, var(--charleston-cream) 92%, transparent);
|
||||
border: 2px solid var(--charleston-iron);
|
||||
box-shadow: 0 4px 20px color-mix(in srgb, var(--charleston-iron) 20%, transparent),
|
||||
inset 0 0 0 1px color-mix(in srgb, var(--charleston-iron) 5%, transparent);
|
||||
color: var(--charleston-iron);
|
||||
}
|
||||
|
||||
/* Lowcountry (Dark Mode) - Warm sunset glow effect */
|
||||
:global(.deepdrft-theme-dark) .player-backdrop {
|
||||
background: color-mix(in srgb, var(--lowcountry-night) 88%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--lowcountry-coral) 50%, transparent);
|
||||
box-shadow: 0 0 20px color-mix(in srgb, var(--lowcountry-coral) 25%, transparent),
|
||||
0 0 40px color-mix(in srgb, var(--lowcountry-twilight) 15%, transparent),
|
||||
0 4px 20px rgba(0, 0, 0, 0.4);
|
||||
color: var(--lowcountry-moonlight);
|
||||
}
|
||||
|
||||
/* 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 rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Charleston (Light Mode) - Iron dock */
|
||||
:global(.deepdrft-theme-light) .minimized-dock {
|
||||
background: linear-gradient(135deg, var(--charleston-iron) 0%, var(--charleston-rose) 50%, var(--charleston-gold) 100%);
|
||||
border: 2px solid var(--charleston-iron);
|
||||
box-shadow: 0 4px 15px color-mix(in srgb, var(--charleston-iron) 40%, transparent);
|
||||
}
|
||||
|
||||
/* Lowcountry (Dark Mode) - Warm sunset dock */
|
||||
:global(.deepdrft-theme-dark) .minimized-dock {
|
||||
background: linear-gradient(135deg, var(--lowcountry-coral) 0%, var(--lowcountry-twilight) 50%, var(--lowcountry-gold) 100%);
|
||||
border: 2px solid color-mix(in srgb, var(--lowcountry-coral) 60%, transparent);
|
||||
box-shadow: 0 0 20px color-mix(in srgb, var(--lowcountry-coral) 40%, transparent),
|
||||
0 0 40px color-mix(in srgb, var(--lowcountry-twilight) 20%, transparent);
|
||||
}
|
||||
|
||||
.minimized-dock:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
:global(.deepdrft-theme-light) .minimized-dock:hover {
|
||||
box-shadow: 0 6px 20px color-mix(in srgb, var(--charleston-iron) 50%, transparent);
|
||||
}
|
||||
|
||||
:global(.deepdrft-theme-dark) .minimized-dock:hover {
|
||||
box-shadow: 0 0 30px color-mix(in srgb, var(--lowcountry-coral) 50%, transparent),
|
||||
0 0 50px color-mix(in srgb, var(--lowcountry-twilight) 30%, transparent);
|
||||
}
|
||||
|
||||
/* 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-visualizer-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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 DeepDrftPublic.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,12 @@
|
||||
@namespace DeepDrftPublic.Client.Controls.AudioPlayerBar
|
||||
|
||||
<div class="spectrum-container @(IsVisible ? "" : "hidden")">
|
||||
<div class="spectrum-bars">
|
||||
@for (int i = 0; i < BucketCount; i++)
|
||||
{
|
||||
var index = i;
|
||||
var height = GetBarHeight(index);
|
||||
<div class="spectrum-bar" style="--bar-height: @(height.ToString("F1"))%;"></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,114 @@
|
||||
using DeepDrftPublic.Client.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
||||
|
||||
public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
|
||||
{
|
||||
[Inject] public required AudioInteropService AudioInterop { get; set; }
|
||||
|
||||
[CascadingParameter] public required IStreamingPlayerService PlayerService { get; set; }
|
||||
|
||||
[Parameter] public int BucketCount { get; set; } = 32;
|
||||
|
||||
private readonly string _instanceId = Guid.NewGuid().ToString();
|
||||
private double[] _spectrumData = Array.Empty<double>();
|
||||
private bool _isAnimating = false;
|
||||
private string? _playerId;
|
||||
private EventCallback? _originalOnStateChanged;
|
||||
|
||||
private bool IsVisible => PlayerService.IsPlaying || PlayerService.IsPaused || _isAnimating;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_spectrumData = new double[BucketCount];
|
||||
|
||||
// Get the player ID from the service
|
||||
if (PlayerService is AudioPlayerService baseService)
|
||||
{
|
||||
_playerId = baseService.PlayerId;
|
||||
}
|
||||
|
||||
// Chain into the existing OnStateChanged callback to detect play/pause
|
||||
_originalOnStateChanged = PlayerService.OnStateChanged;
|
||||
PlayerService.OnStateChanged = new EventCallback(this, async () =>
|
||||
{
|
||||
// Call original callback first
|
||||
if (_originalOnStateChanged.HasValue)
|
||||
{
|
||||
await _originalOnStateChanged.Value.InvokeAsync();
|
||||
}
|
||||
// Then update our animation state
|
||||
await UpdateAnimationState();
|
||||
});
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// Initial check in case already playing
|
||||
await UpdateAnimationState();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateAnimationState()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_playerId)) return;
|
||||
|
||||
var shouldAnimate = PlayerService.IsPlaying;
|
||||
|
||||
if (shouldAnimate && !_isAnimating)
|
||||
{
|
||||
await StartAnimation();
|
||||
}
|
||||
else if (!shouldAnimate && _isAnimating)
|
||||
{
|
||||
await StopAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartAnimation()
|
||||
{
|
||||
if (_isAnimating || string.IsNullOrEmpty(_playerId)) return;
|
||||
|
||||
_isAnimating = true;
|
||||
await AudioInterop.StartSpectrumAnimationAsync(_playerId, _instanceId, OnSpectrumData);
|
||||
}
|
||||
|
||||
private async Task StopAnimation()
|
||||
{
|
||||
if (!_isAnimating || string.IsNullOrEmpty(_playerId)) return;
|
||||
|
||||
_isAnimating = false;
|
||||
await AudioInterop.StopSpectrumAnimationAsync(_playerId, _instanceId);
|
||||
|
||||
// Clear the display
|
||||
Array.Clear(_spectrumData);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private Task OnSpectrumData(double[] data)
|
||||
{
|
||||
if (data.Length > 0)
|
||||
{
|
||||
_spectrumData = data;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private double GetBarHeight(int index)
|
||||
{
|
||||
if (index >= _spectrumData.Length) return 0;
|
||||
|
||||
// Scale to 0-100 percentage, with minimum height for visual appeal
|
||||
var value = _spectrumData[index];
|
||||
return Math.Max(2, value * 100);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await StopAnimation();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
.spectrum-container {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spectrum-container.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.spectrum-bars {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
gap: 2px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.spectrum-bar {
|
||||
flex: 1;
|
||||
margin: 0 auto;
|
||||
max-width: 8px;
|
||||
min-width: 4px;
|
||||
height: var(--bar-height, 2%);
|
||||
min-height: 2px;
|
||||
background: var(--deepdrft-theme-secondary);
|
||||
border-radius: 2px 2px 0 0;
|
||||
transition: height 0.05s ease-out;
|
||||
}
|
||||
|
||||
/* Charleston (Light Mode) - Iron to gold colored bars */
|
||||
:global(.deepdrft-theme-light) .spectrum-bar {
|
||||
background: linear-gradient(to top, var(--charleston-iron) 0%, var(--charleston-rose) 50%, var(--charleston-gold) 100%);
|
||||
}
|
||||
|
||||
/* Lowcountry (Dark Mode) - Coral to gold bars with warm glow */
|
||||
:global(.deepdrft-theme-dark) .spectrum-bar {
|
||||
background: linear-gradient(to top, var(--lowcountry-coral) 0%, var(--lowcountry-gold) 100%);
|
||||
box-shadow: 0 0 4px color-mix(in srgb, var(--lowcountry-gold) 40%, transparent);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.spectrum-container {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.spectrum-bars {
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.spectrum-bar {
|
||||
max-width: 8px;
|
||||
min-width: 3px;
|
||||
}
|
||||
}
|
||||
@@ -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 DeepDrftPublic.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 DeepDrftPublic.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;
|
||||
}
|
||||
Reference in New Issue
Block a user