import { useContext, useEffect, useState } from "react"; import { API_URL } from "../config/api.ts"; import type { Dump } from "../model.ts"; import { formatBytes } from "../utils/format.ts"; import { IconPause, IconPlay, MediaPlayer } from "./MediaPlayer.tsx"; import { PlayerContext } from "../contexts/PlayerContext.ts"; import { BAR_GAP, BAR_W, extractPeaks, NUM_BARS, VIEWBOX_W, WAVEFORM_H, } from "../utils/waveform.ts"; interface FilePreviewProps { dump: Dump; compact?: boolean; global?: boolean; } // Waveform preview for the dump detail page — routes to global player, // reflects live play state and position from PlayerContext. function AudioFilePreview( { fileUrl, mime, dump }: { fileUrl: string; mime: string; dump: Dump }, ) { const { current, playing, currentTime, duration, play, togglePlay, seekTo } = useContext(PlayerContext); const [peaks, setPeaks] = useState(null); const isActive = current?.kind === "file" && current.fileUrl === fileUrl; const progress = isActive && duration > 0 ? currentTime / duration : 0; useEffect(() => { let cancelled = false; extractPeaks(fileUrl, NUM_BARS) .then((p) => { if (!cancelled) setPeaks(p); }) .catch(() => {}); return () => { cancelled = true; }; }, [fileUrl]); const handlePlayBtn = () => { if (isActive) togglePlay(); else play({ kind: "file", fileUrl, mimeType: mime, title: dump.title }); }; const handleWaveformClick = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); const ratio = Math.max( 0, Math.min(1, (e.clientX - rect.left) / rect.width), ); if (isActive) { seekTo(ratio * duration); } else { // Start playing and seek once it loads — seekTo after play() is a no-op // until MediaPlayer mounts; the fraction is best-effort on first click play({ kind: "file", fileUrl, mimeType: mime, title: dump.title }); } }; const isPlaying = isActive && playing; return (
{peaks ? ( {Array.from(peaks).map((p, i) => { const barH = Math.max(p * WAVEFORM_H, 2); const x = i * (BAR_W + BAR_GAP); const y = (WAVEFORM_H - barH) / 2; const played = i / NUM_BARS <= progress; return ( ); })} ) : (
)}
); } function VideoThumb({ src, fallback }: { src: string; fallback: string }) { const [failed, setFailed] = useState(false); if (failed) { return {fallback}; } return ( setFailed(true)} /> ); } function mimeIcon(mime: string): string { if (mime.startsWith("video/")) return "🎬"; if (mime.startsWith("audio/")) return "🎵"; if (mime === "application/pdf") return "📄"; if (mime.startsWith("text/")) return "📝"; return "📁"; } export default function FilePreview( { dump, compact = false, global: useGlobal = false }: FilePreviewProps, ) { const { current, playing, play, togglePlay } = useContext(PlayerContext); const fileUrl = `${API_URL}/api/files/${dump.id}?v=${dump.fileSize ?? 0}`; const mime = dump.fileMime ?? ""; const isPlaying = current?.kind === "file" && current.fileUrl === fileUrl; if (compact) { if (mime.startsWith("image/")) { return ( {dump.fileName} { (e.target as HTMLImageElement).style.display = "none"; }} /> ); } if (mime.startsWith("video/")) { const thumbUrl = `${API_URL}/api/thumbnails/${dump.id}`; return ( ); } if (mime.startsWith("audio/")) { return ( ); } return {mimeIcon(mime)}; } if (mime.startsWith("image/")) { return ( {dump.fileName} ); } if (mime.startsWith("video/")) { if (useGlobal) { const videoActive = isPlaying; const videoPlaying = videoActive && playing; return ( ); } return ; } if (mime.startsWith("audio/")) { if (useGlobal) { return ; } return ; } if (mime === "application/pdf") { return ( ); } return ( {mimeIcon(mime)} Download {dump.fileName} {dump.fileSize != null && ` (${formatBytes(dump.fileSize)})`} ); }