Files
midrandbooks/MidrandBooks/Components/AudiobookPlayer.razor
T
2026-07-19 09:54:35 +02:00

309 lines
16 KiB
Plaintext

@namespace MidrandBooks.Components
@using Microsoft.AspNetCore.Components
@using MidrandBooks.Models
@if (Book != null)
{
<div class="fixed bottom-0 left-0 right-0 z-50 p-4 transition-all duration-300">
<div class="max-w-4xl mx-auto rounded-3xl border border-slate-800/40 bg-slate-900 shadow-2xl p-5 text-white relative overflow-hidden transition-all duration-300 @(IsMinimized ? "max-h-[85px]" : "max-h-[500px]")">
<!-- Atmospheric ambient blur background -->
<div class="absolute inset-0 bg-gradient-to-tr @Book.CoverColor opacity-10 blur-xl scale-125 pointer-events-none -z-10" />
<!-- Minimized View Layout -->
<div class="flex items-center justify-between gap-4">
<!-- Jacket and Title -->
<div class="flex items-center gap-3 min-w-0">
<div class="h-12 w-9 rounded overflow-hidden shadow-md flex-shrink-0 border border-white/5">
@if (!string.IsNullOrEmpty(Book.CoverUrl))
{
<img src="@Book.CoverUrl" alt="@Book.Title" class="h-full w-full object-cover" />
}
else
{
<div class="h-full w-full bg-gradient-to-br @Book.CoverColor flex items-center justify-center">
<span class="text-[5px] text-white/50">AUDIO</span>
</div>
}
</div>
<div class="min-w-0">
<span class="text-[9px] font-mono font-bold text-violet-400 block uppercase tracking-wider">Now Playing Audiobook</span>
<h4 class="text-xs font-serif font-bold text-white truncate leading-snug">@Book.Title</h4>
<span class="text-[9px] text-slate-400 truncate block">By @Book.Author</span>
</div>
</div>
<!-- Central Mini Controls -->
<div class="flex items-center gap-3">
<button @onclick="TogglePlay" class="h-9 w-9 rounded-full bg-amber-400 text-slate-950 flex items-center justify-center hover:bg-amber-300 hover:scale-105 active:scale-95 transition-all cursor-pointer">
@if (IsPlaying)
{
<svg class="h-4 w-4 fill-slate-950" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
</svg>
}
else
{
<svg class="h-4 w-4 fill-slate-950" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z" />
</svg>
}
</button>
<button @onclick="ToggleMinimize" class="p-2 rounded-xl bg-white/5 hover:bg-white/10 text-slate-400 hover:text-white transition-colors cursor-pointer">
@if (IsMinimized)
{
<svg class="h-4.5 w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
</svg>
}
else
{
<svg class="h-4.5 w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
}
</button>
<button @onclick="OnClose" class="p-2 rounded-xl bg-white/5 hover:bg-white/10 text-slate-400 hover:text-red-400 transition-colors cursor-pointer">
<svg class="h-4.5 w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- Full Expanded Dashboard -->
@if (!IsMinimized)
{
<div class="mt-6 border-t border-white/5 pt-5 grid grid-cols-1 md:grid-cols-12 gap-6 animate-in slide-in-from-bottom duration-200">
<!-- Sleep timer & Speed Controls (4 cols) -->
<div class="md:col-span-4 bg-white/2 rounded-2xl p-4 border border-white/5 space-y-4">
<!-- Playback Speed -->
<div class="space-y-2">
<label class="text-[10px] font-mono font-bold uppercase text-slate-400 tracking-wider">Playback Speed</label>
<div class="grid grid-cols-4 gap-1.5 p-1 bg-white/5 rounded-xl">
@foreach (var spd in Speeds)
{
<button @onclick="() => SetSpeed(spd)" class="py-1 rounded-lg text-[10px] font-bold font-mono transition-all cursor-pointer @(PlaybackSpeed == spd ? "bg-amber-400 text-slate-950" : "text-slate-400 hover:text-white")">
@(spd)x
</button>
}
</div>
</div>
<!-- Sleep Timer -->
<div class="space-y-2">
<label class="text-[10px] font-mono font-bold uppercase text-slate-400 tracking-wider flex justify-between">
<span>Sleep Timer</span>
@if (SleepTimerValue != null)
{
<span class="text-amber-400 font-bold">@FormatSeconds(TimeLeft) Left</span>
}
</label>
<div class="grid grid-cols-4 gap-1.5 p-1 bg-white/5 rounded-xl">
<button @onclick="ClearSleepTimer" class="py-1 rounded-lg text-[10px] font-mono transition-all cursor-pointer @(SleepTimerValue == null ? "bg-white/10 text-white" : "text-slate-400")">Off</button>
<button @onclick="() => SetSleepTimer(15)" class="py-1 rounded-lg text-[10px] font-mono transition-all cursor-pointer @(SleepTimerValue == 15 ? "bg-amber-400 text-slate-950" : "text-slate-400")">15m</button>
<button @onclick="() => SetSleepTimer(30)" class="py-1 rounded-lg text-[10px] font-mono transition-all cursor-pointer @(SleepTimerValue == 30 ? "bg-amber-400 text-slate-950" : "text-slate-400")">30m</button>
<button @onclick="() => SetSleepTimer(60)" class="py-1 rounded-lg text-[10px] font-mono transition-all cursor-pointer @(SleepTimerValue == 60 ? "bg-amber-400 text-slate-950" : "text-slate-400")">60m</button>
</div>
</div>
</div>
<!-- Main progress seeker & volume controls (8 cols) -->
<div class="md:col-span-8 flex flex-col justify-between space-y-4">
<!-- Seek Bar -->
<div class="space-y-1">
<input type="range" min="0" max="@Duration" value="@Progress" @oninput="SeekChanged" class="w-full h-1.5 bg-slate-700 accent-amber-500 rounded-lg appearance-none cursor-pointer transition-colors" />
<div class="flex justify-between text-[10px] font-mono text-slate-400">
<span>@FormatSeconds(Progress)</span>
<span>@FormatSeconds(Duration)</span>
</div>
</div>
<!-- Central Media Controls Rail -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<button @onclick="SkipBack" class="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 hover:text-white transition-all cursor-pointer" title="Rewind 15 seconds">
<svg class="h-4.5 w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12.066 11.2a1 1 0 000 1.6l5.334 4A1 1 0 0019 16V8a1 1 0 00-1.6-.8l-5.334 4zM4.066 11.2a1 1 0 000 1.6l5.334 4A1 1 0 0011 16V8a1 1 0 00-1.6-.8l-5.334 4z" />
</svg>
</button>
<button @onclick="TogglePlay" class="h-12 w-12 rounded-full bg-amber-400 text-slate-950 flex items-center justify-center hover:bg-amber-300 hover:scale-105 active:scale-95 transition-all shadow-lg cursor-pointer">
@if (IsPlaying)
{
<svg class="h-5 w-5 fill-slate-950" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
</svg>
}
else
{
<svg class="h-5 w-5 fill-slate-950" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z" />
</svg>
}
</button>
<button @onclick="SkipForward" class="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 hover:text-white transition-all cursor-pointer" title="Skip 15 seconds">
<svg class="h-4.5 w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.934 12.8a1 1 0 000-1.6l-5.334-4A1 1 0 005 8v8a1 1 0 001.6.8l5.334-4zM19.934 12.8a1 1 0 000-1.6l-5.334-4A1 1 0 0013 8v8a1 1 0 001.6.8l5.334-4z" />
</svg>
</button>
</div>
<!-- Volume Panel -->
<div class="flex items-center gap-2">
<button @onclick="ToggleMute" class="p-2 rounded-xl bg-white/5 hover:bg-white/10 cursor-pointer">
@if (IsMuted || Volume == 0)
{
<svg class="h-4.5 w-4.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
</svg>
}
else
{
<svg class="h-4.5 w-4.5 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
</svg>
}
</button>
<input type="range" min="0" max="1" step="0.05" @bind="Volume" class="w-20 h-1.5 bg-slate-700 accent-amber-500 rounded-lg appearance-none cursor-pointer transition-colors" />
</div>
</div>
</div>
</div>
}
</div>
</div>
}
@code {
[Parameter] public BookModel? Book { get; set; }
[Parameter] public bool IsPlaying { get; set; }
[Parameter] public EventCallback<bool> OnPlayToggle { get; set; }
[Parameter] public EventCallback OnClose { get; set; }
private bool IsMinimized { get; set; } = false;
private double PlaybackSpeed { get; set; } = 1.0;
private double[] Speeds = new double[] { 0.75, 1.0, 1.25, 1.5 };
private double Volume { get; set; } = 0.8;
private bool IsMuted { get; set; } = false;
private double PreviousVolume { get; set; } = 0.8;
private double Progress { get; set; } = 120; // Simulated seconds played
private double Duration { get; set; } = 3600; // Simulated total seconds (1 hour)
// Sleep Timer States
private int? SleepTimerValue { get; set; }
private int TimeLeft { get; set; }
private System.Threading.Timer? SleepEngineTimer { get; set; }
private void ToggleMinimize()
{
IsMinimized = !IsMinimized;
}
private void TogglePlay()
{
IsPlaying = !IsPlaying;
OnPlayToggle.InvokeAsync(IsPlaying);
}
private void SetSpeed(double speed)
{
PlaybackSpeed = speed;
}
private void ToggleMute()
{
if (IsMuted)
{
Volume = PreviousVolume;
IsMuted = false;
}
else
{
PreviousVolume = Volume;
Volume = 0;
IsMuted = true;
}
}
private void SeekChanged(ChangeEventArgs e)
{
if (double.TryParse(e.Value?.ToString(), out var seekVal))
{
Progress = seekVal;
}
}
private void SkipForward()
{
Progress = Math.Min(Duration, Progress + 15);
}
private void SkipBack()
{
Progress = Math.Max(0, Progress - 15);
}
private void SetSleepTimer(int minutes)
{
SleepTimerValue = minutes;
TimeLeft = minutes * 60;
SleepEngineTimer?.Dispose();
SleepEngineTimer = new System.Threading.Timer(_ =>
{
InvokeAsync(() =>
{
if (TimeLeft > 1)
{
TimeLeft--;
}
else
{
IsPlaying = false;
OnPlayToggle.InvokeAsync(false);
SleepTimerValue = null;
SleepEngineTimer?.Dispose();
}
StateHasChanged();
});
}, null, 1000, 1000);
}
private void ClearSleepTimer()
{
SleepTimerValue = null;
SleepEngineTimer?.Dispose();
}
private string FormatSeconds(double totalSecs)
{
var hours = Math.Floor(totalSecs / 3600);
var minutes = Math.Floor((totalSecs % 3600) / 60);
var seconds = Math.Floor(totalSecs % 60);
var formattedSecs = seconds < 10 ? $"0{seconds}" : seconds.ToString();
if (hours > 0)
{
var formattedMins = minutes < 10 ? $"0{minutes}" : minutes.ToString();
return $"{hours}:{formattedMins}:{formattedSecs}";
}
return $"{minutes}:{formattedSecs}";
}
public void Dispose()
{
SleepEngineTimer?.Dispose();
}
}