"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import type { AudioTrack } from "@/types"; import { api } from "@/lib/api"; interface Props { pdfId: number; tracks: AudioTrack[]; /** Called by parent after a successful upload so the track list refreshes */ onTracksChange: (tracks: AudioTrack[]) => void; userRole: "admin" | "teacher" | "student"; } function fmt(sec: number): string { if (!isFinite(sec)) return "0:00"; const m = Math.floor(sec / 60); const s = Math.floor(sec % 60); return `${m}:${s.toString().padStart(2, "0")}`; } export default function FloatingAudioPlayer({ pdfId, tracks, onTracksChange, userRole }: Props) { const audioRef = useRef(null); const [currentIdx, setCurrentIdx] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [showPlaylist, setShowPlaylist] = useState(false); const [uploading, setUploading] = useState(false); const [deleting, setDeleting] = useState(null); const fileInputRef = useRef(null); const canManage = userRole === "admin" || userRole === "teacher"; const multiTrack = tracks.length > 1; const currentTrack = tracks[currentIdx] ?? null; const srcUrl = currentTrack ? api.audioFileUrl(pdfId, currentTrack.id) : null; // Sync audio element src when track changes useEffect(() => { const audio = audioRef.current; if (!audio || !srcUrl) return; audio.pause(); audio.load(); setIsPlaying(false); setCurrentTime(0); setDuration(0); }, [srcUrl]); const togglePlay = useCallback(async () => { const audio = audioRef.current; if (!audio) return; if (isPlaying) { audio.pause(); } else { await audio.play().catch(() => {}); } }, [isPlaying]); const rewind5 = useCallback(() => { const audio = audioRef.current; if (!audio) return; audio.currentTime = Math.max(0, audio.currentTime - 5); }, []); const handleSeek = useCallback((e: React.ChangeEvent) => { const audio = audioRef.current; if (!audio) return; audio.currentTime = Number(e.target.value); }, []); const handleTimeUpdate = useCallback(() => { setCurrentTime(audioRef.current?.currentTime ?? 0); }, []); const handleLoadedMetadata = useCallback(() => { setDuration(audioRef.current?.duration ?? 0); }, []); const handleEnded = useCallback(() => { if (multiTrack && currentIdx < tracks.length - 1) { setCurrentIdx((i) => i + 1); } else { setIsPlaying(false); } }, [multiTrack, currentIdx, tracks.length]); // Auto-play next track when index changes useEffect(() => { if (isPlaying) { audioRef.current?.play().catch(() => {}); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentIdx]); // File upload handler const handleFileUpload = useCallback( async (e: React.ChangeEvent) => { const files = Array.from(e.target.files ?? []); if (!files.length) return; setUploading(true); try { const meta = files.map((f, i) => ({ trackName: f.name.replace(/\.[^.]+$/, ""), sortOrder: tracks.length + i + 1, })); const results = await api.uploadAudio(pdfId, files, meta); const newTracks = results.filter((r) => r.success && r.audio).map((r) => r.audio!); if (newTracks.length) { const updated = await api.listAudio(pdfId); onTracksChange(updated); } const errors = results.filter((r) => !r.success); if (errors.length) { alert(errors.map((r) => `${r.filename}: ${r.error}`).join("\n")); } } catch (err) { alert(err instanceof Error ? err.message : "Upload failed."); } finally { setUploading(false); if (fileInputRef.current) fileInputRef.current.value = ""; } }, [pdfId, tracks.length, onTracksChange], ); const handleDelete = useCallback( async (trackId: number) => { if (!confirm("Xóa track này?")) return; setDeleting(trackId); try { await api.deleteAudio(pdfId, trackId); const updated = await api.listAudio(pdfId); onTracksChange(updated); if (currentTrack?.id === trackId) setCurrentIdx(0); } catch (err) { alert(err instanceof Error ? err.message : "Delete failed."); } finally { setDeleting(null); } }, [pdfId, currentTrack, onTracksChange], ); if (!tracks.length && !canManage) return null; return (
{/* Hidden native audio element */} {srcUrl && (
); } // ── Style helpers ───────────────────────────────────────────────────────────── function iconBtn(color: string): React.CSSProperties { return { background: "none", border: "none", color, cursor: "pointer", fontSize: 16, padding: "2px 4px", lineHeight: 1, }; } function ctrlBtn(disabled: boolean): React.CSSProperties { return { background: "none", border: "none", color: disabled ? "#475569" : "#e2e8f0", cursor: disabled ? "not-allowed" : "pointer", fontSize: 20, padding: "2px 4px", lineHeight: 1, width: 32, height: 32, borderRadius: 6, }; }