Stable run with tailwind
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
@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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user