From 7007d2df93e1b9076f71ce0b7580eff69a568d47 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Fri, 22 May 2026 08:17:17 -0400 Subject: [PATCH] Add Z-AMPP UI: screens-asset + screens-projects: screens-asset.jsx --- services/web-ui/public/screens-asset.jsx | 400 +++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 services/web-ui/public/screens-asset.jsx diff --git a/services/web-ui/public/screens-asset.jsx b/services/web-ui/public/screens-asset.jsx new file mode 100644 index 0000000..b66e1cc --- /dev/null +++ b/services/web-ui/public/screens-asset.jsx @@ -0,0 +1,400 @@ +// screens-asset.jsx — asset detail (Frame.io-style player, filmstrip, comments) + +const { COMMENTS: SEED_COMMENTS } = window.ZAMPP_DATA; + +function AssetDetail({ asset, onClose }) { + const [playing, setPlaying] = React.useState(false); + const [currentMs, setCurrentMs] = React.useState(720000); + 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) 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, 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} + + v3 +
+
+ {asset.project}·{asset.bin}·updated {asset.updated} +
+
+
+ + + + +
+ +
+
+
+
+ +
+ {!playing && ( + + )} +
+ {visibleComments + .filter(c => Math.abs(parseDuration(c.time) - currentMs) < 200) + .map(c => ( +
+
{c.avatar}
+
{c.text}
+
+ ))} +
+ {asset.status === "live" && ( +
+ LIVE · REC +
+ )} +
+ {msToTimecode(currentMs)} + / {asset.duration} +
+
+ +
+ + {msToTimecode(currentMs)} + + {asset.duration} +
+ + + +
+ + +
+ +
+ + + + + +
+ {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" && } + {tab === "activity" && } +
+
+ +
+ +
+
+
+ ); +} + +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 = (current / total) * 100; + return ( +
+
+
+ {comments.map(c => { + const x = (parseDuration(c.time) / total) * 100; + return ( +
{ e.stopPropagation(); onSeek(parseDuration(c.time)); }} + > + {c.avatar} +
+ ); + })} +
+ ); +} + +function FilmStrip({ seed, current, total, onSeek, comments }) { + const ref = React.useRef(null); + const frames = 28; + const handle = (e) => { + const r = ref.current.getBoundingClientRect(); + onSeek(((e.clientX - r.left) / r.width) * total); + }; + return ( +
+
+ {Array.from({ length: frames }).map((_, i) => ( +
+ +
+ ))} +
+ {comments.map(c => { + const x = (parseDuration(c.time) / 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 +
+ {["KM", "ZG", "MO", "JT"].map((a, i) => ( +
{a}
+ ))} + +
+
+
+ @ {msToTimecode(currentMs)} + Drawing tools +
+