// screens-asset.jsx — asset detail (Frame.io-style player, filmstrip, comments) const { COMMENTS: SEED_COMMENTS } = window.ZAMPP_DATA; // Simple gradient palette — replaces the missing thumbGrad function const _FRAME_GRADIENTS = [ 'linear-gradient(135deg,#1a1f2e 0%,#2a3045 100%)', 'linear-gradient(135deg,#1e2030 0%,#2d2040 100%)', 'linear-gradient(135deg,#0d1520 0%,#1a2535 100%)', 'linear-gradient(135deg,#1f1a2e 0%,#2a2040 100%)', 'linear-gradient(135deg,#0f1e18 0%,#1a3028 100%)', 'linear-gradient(135deg,#1e1510 0%,#302018 100%)', 'linear-gradient(135deg,#1a1020 0%,#281830 100%)', 'linear-gradient(135deg,#101828 0%,#182438 100%)', 'linear-gradient(135deg,#1e2820 0%,#283830 100%)', 'linear-gradient(135deg,#201820 0%,#302030 100%)', 'linear-gradient(135deg,#181e28 0%,#202838 100%)', ]; function AssetDetail({ asset, onClose }) { const [playing, setPlaying] = React.useState(false); const [currentMs, setCurrentMs] = React.useState(0); const [tab, setTab] = React.useState("comments"); const [showResolved, setShowResolved] = React.useState(false); const [comments, setComments] = React.useState(SEED_COMMENTS || []); const [newComment, setNewComment] = React.useState(""); // Stream / video state const [streamUrl, setStreamUrl] = React.useState(null); const [streamType, setStreamType] = React.useState(null); const [streamLoading, setStreamLoading] = React.useState(false); const [videoDuration, setVideoDuration] = React.useState(0); const [retrying, setRetrying] = React.useState(false); const videoRef = React.useRef(null); const assetId = asset && asset.id; const totalMs = videoDuration > 0 ? videoDuration : parseDuration(asset.duration); // Fetch stream URL when asset changes React.useEffect(() => { if (!assetId) return; setStreamUrl(null); setStreamType(null); setVideoDuration(0); setCurrentMs(0); setPlaying(false); setStreamLoading(true); window.ZAMPP_API.fetch('/assets/' + assetId + '/stream') .then(function(r) { if (r && r.url) { setStreamUrl(r.url); setStreamType(r.type || 'mp4'); } }) .catch(function() {}) .finally(function() { setStreamLoading(false); }); }, [assetId]); // Wire hls.js for live HLS streams React.useEffect(() => { if (!streamUrl || streamType !== 'hls' || !videoRef.current) return; if (!window.Hls) return; const hls = new window.Hls(); hls.loadSource(streamUrl); hls.attachMedia(videoRef.current); return function() { hls.destroy(); }; }, [streamUrl, streamType]); // Fake playback timer — only used when no real video stream React.useEffect(() => { if (!playing || totalMs <= 0 || streamUrl) return; const i = setInterval(function() { setCurrentMs(function(t) { const next = t + 100; if (next >= totalMs) { setPlaying(false); return totalMs; } return next; }); }, 100); return function() { clearInterval(i); }; }, [playing, totalMs, streamUrl]); const togglePlay = function() { if (videoRef.current) { if (videoRef.current.paused) { videoRef.current.play(); } else { videoRef.current.pause(); } } else { setPlaying(function(p) { return !p; }); } }; const seek = function(ms) { const clamped = Math.max(0, Math.min(totalMs || 0, ms)); setCurrentMs(clamped); if (videoRef.current) videoRef.current.currentTime = clamped / 1000; }; const retryProcessing = function() { if (retrying) return; setRetrying(true); window.ZAMPP_API.fetch('/assets/' + assetId + '/retry', { method: 'POST' }) .then(function() { window.alert('Re-queued for processing. Refresh in a moment.'); }) .catch(function(e) { window.alert('Retry failed: ' + (e.message || 'unknown error')); }) .finally(function() { setRetrying(false); }); }; const addComment = function() { if (!newComment.trim()) return; const t = msToTimecode(currentMs); setComments(function(c) { return [...c, { id: 'n' + Date.now(), who: "Zach Gaetano", avatar: "ZG", time: t, real: "just now", text: newComment, resolved: false, frame: Math.floor(currentMs / 1000 * 30), }]; }); setNewComment(""); }; const visibleComments = comments.filter(function(c) { return showResolved || !c.resolved; }); const statusMessage = asset.status === 'processing' ? 'Processing…' : asset.status === 'live' ? 'Live recording in progress' : asset.status === 'error' ? 'Processing failed' : 'Preview not yet available'; return (