309 lines
16 KiB
Plaintext
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 bg-white/10 rounded-lg appearance-none cursor-pointer" />
|
|
<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 bg-white/10 rounded-lg appearance-none cursor-pointer" />
|
|
</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();
|
|
}
|
|
}
|