feat: shared player with 6s cap, progress bar, stop button
All checks were successful
Deploy / build-and-deploy (push) Successful in 1m43s
All checks were successful
Deploy / build-and-deploy (push) Successful in 1m43s
This commit is contained in:
@@ -9,6 +9,7 @@ import { SOUND_CATEGORIES, EMOJIS } from "@/lib/bingo-data";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { ResourceBrowser, DragData } from "./ResourceBrowser";
|
||||
import { playSoundOnce } from "@/lib/sounds";
|
||||
import { SoundPlayerBar } from "./SoundPlayerBar";
|
||||
|
||||
type Item = {
|
||||
id: string;
|
||||
@@ -222,7 +223,7 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
|
||||
if (!item) return;
|
||||
|
||||
if (data.type === "sound") {
|
||||
playSoundOnce(data.url);
|
||||
playSoundOnce(data.url, data.originalName);
|
||||
await updateItem({ ...item, soundCategory: "custom", soundUrl: data.url });
|
||||
} else if (data.type === "image") {
|
||||
await updateItem({ ...item, imageUrl: data.url });
|
||||
@@ -317,7 +318,7 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
|
||||
{editSoundUrl && (
|
||||
<div className="flex items-center gap-1 text-[10px] font-mono text-cyan-400">
|
||||
<span>✓ sound loaded</span>
|
||||
<button type="button" onClick={() => playSoundOnce(editSoundUrl)} className="px-1.5 py-0.5 rounded bg-slate-700/50 hover:bg-slate-600/50 text-xs cursor-pointer">▶</button>
|
||||
<button type="button" onClick={() => playSoundOnce(editSoundUrl!, "Preview")} className="px-1.5 py-0.5 rounded bg-slate-700/50 hover:bg-slate-600/50 text-xs cursor-pointer">▶</button>
|
||||
<button type="button" onClick={() => setEditSoundUrl(null)} className="px-1.5 py-0.5 rounded bg-slate-700/50 hover:bg-red-800/50 text-xs cursor-pointer">✕</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -374,7 +375,7 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
|
||||
{item.soundUrl && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={e => { e.stopPropagation(); playSoundOnce(item.soundUrl!); }}
|
||||
onClick={e => { e.stopPropagation(); playSoundOnce(item.soundUrl!, item.text); }}
|
||||
className="text-[10px] text-cyan-500/60 hover:text-cyan-300 cursor-pointer relative z-10"
|
||||
>
|
||||
🔊
|
||||
@@ -396,6 +397,9 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) {
|
||||
{/* Resource Browser dock */}
|
||||
<ResourceBrowser visible={showBrowser} onClose={() => setShowBrowser(false)} />
|
||||
|
||||
{/* Sound playback bar */}
|
||||
<SoundPlayerBar />
|
||||
|
||||
{/* All Items list */}
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-[10px] font-mono text-slate-600 uppercase">All Items</h4>
|
||||
|
||||
@@ -198,7 +198,7 @@ function SoundsTab() {
|
||||
label={s.originalName}
|
||||
duration={s.duration}
|
||||
url={s.url}
|
||||
onPlay={() => playSoundOnce(s.url)}
|
||||
onPlay={() => playSoundOnce(s.url, s.originalName)}
|
||||
onDelete={() => deleteSound(filename)}
|
||||
/>
|
||||
);
|
||||
@@ -231,7 +231,7 @@ function SoundsTab() {
|
||||
key={r.id}
|
||||
name={r.name}
|
||||
duration={r.duration}
|
||||
onPreview={() => r.previewUrl && playSoundOnce(r.previewUrl)}
|
||||
onPreview={() => r.previewUrl && playSoundOnce(r.previewUrl, r.name)}
|
||||
onImport={() => importSound(r)}
|
||||
/>
|
||||
))}
|
||||
|
||||
42
components/SoundPlayerBar.tsx
Normal file
42
components/SoundPlayerBar.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { subscribe, getPlaybackState, stopCurrent, PlaybackState } from "@/lib/player";
|
||||
|
||||
export function SoundPlayerBar() {
|
||||
const [s, setS] = useState<PlaybackState>(getPlaybackState());
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = subscribe(setS);
|
||||
return unsub;
|
||||
}, []);
|
||||
|
||||
if (!s.isPlaying) return null;
|
||||
|
||||
const pct = Math.min((s.elapsed / s.duration) * 100, 100);
|
||||
|
||||
return (
|
||||
<div className="border-t border-slate-700/30 bg-slate-950/95 px-3 py-2">
|
||||
<div className="flex items-center gap-2 text-[10px] font-mono max-w-2xl mx-auto">
|
||||
<span className="text-cyan-400 shrink-0">🔊</span>
|
||||
<span className="text-slate-300 truncate max-w-[200px]">{s.name}</span>
|
||||
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden min-w-[80px]">
|
||||
<div
|
||||
className="h-full bg-cyan-500 transition-all duration-100 rounded-full"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-slate-500 shrink-0 w-16 text-right tabular-nums">
|
||||
{s.elapsed.toFixed(1)}s / {s.duration.toFixed(0)}s
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={stopCurrent}
|
||||
className="px-2 py-0.5 rounded bg-slate-800 hover:bg-red-800/60 text-slate-400 hover:text-red-300 cursor-pointer shrink-0"
|
||||
>
|
||||
■
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user