// 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("");
const totalMs = parseDuration(asset.duration);
React.useEffect(() => {
if (!playing || totalMs <= 0) return;
const i = setInterval(() => {
setCurrentMs(t => {
const next = t + 100;
if (next >= totalMs) { setPlaying(false); return totalMs; }
return next;
});
}, 100);
return () => clearInterval(i);
}, [playing, totalMs]);
const seek = (ms) => setCurrentMs(Math.max(0, Math.min(totalMs || 0, ms)));
const addComment = () => {
if (!newComment.trim()) return;
const t = msToTimecode(currentMs);
setComments(c => [...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(c => showResolved || !c.resolved);
return (
{asset.name}
{asset.project}·updated {asset.updated}
{!playing && totalMs > 0 && (
)}
{totalMs <= 0 && (
{asset.status === 'processing' ? 'Processing…' : asset.status === 'live' ? 'Live recording in progress' : 'Preview not yet available'}
)}
{visibleComments
.filter(c => Math.abs(parseDuration(c.time) - currentMs) < 200)
.map(c => (
))}
{asset.status === "live" && (
LIVE · REC
)}
{totalMs > 0 && (
{msToTimecode(currentMs)}
/ {asset.duration}
)}
{totalMs > 0 && (
{msToTimecode(currentMs)}
{asset.duration}
)}
{totalMs > 0 && (
)}
{tab === "comments" && (
)}
{tab === "comments" && (
seek(parseDuration(c.time))} onResolve={(id) => {
setComments(cs => cs.map(c => c.id === id ? { ...c, resolved: !c.resolved } : c));
}} />
)}
{tab === "versions" && }
{tab === "metadata" && }
{tab === "audio" && }
);
}
function PlaybackBar({ current, total, onSeek, comments }) {
const ref = React.useRef(null);
const handle = (e) => {
const r = ref.current.getBoundingClientRect();
const p = (e.clientX - r.left) / r.width;
onSeek(Math.max(0, Math.min(1, p)) * total);
};
const pct = total > 0 ? (current / total) * 100 : 0;
return (
{comments.map(c => {
const ct = parseDuration(c.time);
if (!ct || total <= 0) return null;
const x = (ct / total) * 100;
return (
{ e.stopPropagation(); onSeek(ct); }}
>
{c.avatar}
);
})}
);
}
function FilmStrip({ seed, current, total, onSeek, comments }) {
const ref = React.useRef(null);
const frames = 28;
const handle = (e) => {
if (!ref.current || total <= 0) return;
const r = ref.current.getBoundingClientRect();
onSeek(((e.clientX - r.left) / r.width) * total);
};
const pct = total > 0 ? (current / total) * 100 : 0;
return (
{Array.from({ length: frames }).map((_, i) => (
))}
{comments.map(c => {
const ct = parseDuration(c.time);
if (!ct || total <= 0) return null;
const x = (ct / total) * 100;
return (
{c.avatar}
);
})}
{[0, 0.25, 0.5, 0.75, 1].map(p => (
{msToTimecode(p * total)}
))}
);
}
function CommentsList({ comments, onSeek, onResolve }) {
if (comments.length === 0) {
return No comments yet.
;
}
return (
{comments.map(c => (
{c.avatar}
{c.who}
{c.real}
{c.text}
{c.resolved &&
✓ Resolved
}
))}
);
}
function CommentComposer({ asset, currentMs, value, onChange, onSubmit }) {
return (
Reviewers
{["ZG"].map((a, i) => (
{a}
))}
@ {msToTimecode(currentMs)}
);
}
function VersionsTab() {
return (
Version history not yet available.
);
}
function MetadataTab({ asset }) {
const rows = [
{ k: "Filename", v: asset.name },
{ k: "Duration", v: asset.duration || '—' },
{ k: "Resolution", v: asset.res || '—' },
{ k: "Codec", v: asset.codec || '—' },
{ k: "File size", v: asset.size || '—' },
{ k: "Status", v: asset.status || '—' },
{ k: "Updated", v: asset.updated || '—' },
{ k: "Project", v: asset.project || '—' },
];
return (
);
}
function AudioTab({ asset }) {
return (
);
}
function parseDuration(d) {
if (!d || d === '—' || typeof d !== 'string') return 0;
const parts = d.split(':');
if (parts.length < 2) return 0;
const nums = parts.map(Number);
if (nums.some(isNaN)) return 0;
if (nums.length === 3) return ((nums[0] * 60 + nums[1]) * 60 + nums[2]) * 1000;
return (nums[0] * 60 + nums[1]) * 1000;
}
function msToTimecode(ms) {
const total = Math.floor((ms || 0) / 1000);
const h = Math.floor(total / 3600);
const m = Math.floor((total % 3600) / 60);
const s = total % 60;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}
function avatarColor(initials) {
let h = 0;
for (const c of (initials || '?')) h = (h * 31 + c.charCodeAt(0)) & 0xff;
return `linear-gradient(135deg, hsl(${h % 360} 60% 50%), hsl(${(h + 60) % 360} 60% 45%))`;
}
Object.assign(window, { AssetDetail, msToTimecode, parseDuration, avatarColor });