screens-asset: fix thumbGrad crash, parseDuration NaN, guard missing ACTIVITY
This commit is contained in:
parent
451bed834f
commit
bec58ab138
1 changed files with 92 additions and 108 deletions
|
|
@ -2,18 +2,33 @@
|
||||||
|
|
||||||
const { COMMENTS: SEED_COMMENTS } = window.ZAMPP_DATA;
|
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 }) {
|
function AssetDetail({ asset, onClose }) {
|
||||||
const [playing, setPlaying] = React.useState(false);
|
const [playing, setPlaying] = React.useState(false);
|
||||||
const [currentMs, setCurrentMs] = React.useState(720000);
|
const [currentMs, setCurrentMs] = React.useState(0);
|
||||||
const [tab, setTab] = React.useState("comments");
|
const [tab, setTab] = React.useState("comments");
|
||||||
const [showResolved, setShowResolved] = React.useState(false);
|
const [showResolved, setShowResolved] = React.useState(false);
|
||||||
const [comments, setComments] = React.useState(SEED_COMMENTS);
|
const [comments, setComments] = React.useState(SEED_COMMENTS || []);
|
||||||
const [newComment, setNewComment] = React.useState("");
|
const [newComment, setNewComment] = React.useState("");
|
||||||
|
|
||||||
const totalMs = parseDuration(asset.duration);
|
const totalMs = parseDuration(asset.duration);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!playing) return;
|
if (!playing || totalMs <= 0) return;
|
||||||
const i = setInterval(() => {
|
const i = setInterval(() => {
|
||||||
setCurrentMs(t => {
|
setCurrentMs(t => {
|
||||||
const next = t + 100;
|
const next = t + 100;
|
||||||
|
|
@ -24,7 +39,7 @@ function AssetDetail({ asset, onClose }) {
|
||||||
return () => clearInterval(i);
|
return () => clearInterval(i);
|
||||||
}, [playing, totalMs]);
|
}, [playing, totalMs]);
|
||||||
|
|
||||||
const seek = (ms) => setCurrentMs(Math.max(0, Math.min(totalMs, ms)));
|
const seek = (ms) => setCurrentMs(Math.max(0, Math.min(totalMs || 0, ms)));
|
||||||
const addComment = () => {
|
const addComment = () => {
|
||||||
if (!newComment.trim()) return;
|
if (!newComment.trim()) return;
|
||||||
const t = msToTimecode(currentMs);
|
const t = msToTimecode(currentMs);
|
||||||
|
|
@ -51,14 +66,12 @@ function AssetDetail({ asset, onClose }) {
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<span style={{ fontWeight: 600, fontSize: 14 }}>{asset.name}</span>
|
<span style={{ fontWeight: 600, fontSize: 14 }}>{asset.name}</span>
|
||||||
<StatusDot status={asset.status} />
|
<StatusDot status={asset.status} />
|
||||||
<span className="badge accent">v3</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 11.5, color: "var(--text-3)", display: "flex", gap: 6 }}>
|
<div style={{ fontSize: 11.5, color: "var(--text-3)", display: "flex", gap: 6 }}>
|
||||||
<span>{asset.project}</span><span>·</span><span>{asset.bin}</span><span>·</span><span>updated {asset.updated}</span>
|
<span>{asset.project}</span><span>·</span><span>updated {asset.updated}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
<button className="btn ghost sm"><Icon name="share" />Share</button>
|
|
||||||
<button className="btn ghost sm"><Icon name="download" />Download</button>
|
<button className="btn ghost sm"><Icon name="download" />Download</button>
|
||||||
<button className="btn subtle sm"><Icon name="check" />Approve</button>
|
<button className="btn subtle sm"><Icon name="check" />Approve</button>
|
||||||
<button className="icon-btn"><Icon name="more" /></button>
|
<button className="icon-btn"><Icon name="more" /></button>
|
||||||
|
|
@ -70,11 +83,21 @@ function AssetDetail({ asset, onClose }) {
|
||||||
<div className="player-canvas">
|
<div className="player-canvas">
|
||||||
<FauxFrame seed={asset.seed || 1} />
|
<FauxFrame seed={asset.seed || 1} />
|
||||||
<div className="scanlines" />
|
<div className="scanlines" />
|
||||||
{!playing && (
|
{!playing && totalMs > 0 && (
|
||||||
<button className="player-play-overlay" onClick={() => setPlaying(true)}>
|
<button className="player-play-overlay" onClick={() => setPlaying(true)}>
|
||||||
<Icon name="play" size={28} />
|
<Icon name="play" size={28} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{totalMs <= 0 && (
|
||||||
|
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<div style={{ textAlign: 'center', color: 'var(--text-3)' }}>
|
||||||
|
<div style={{ fontSize: 13, marginBottom: 8 }}>
|
||||||
|
{asset.status === 'processing' ? 'Processing…' : asset.status === 'live' ? 'Live recording in progress' : 'Preview not yet available'}
|
||||||
|
</div>
|
||||||
|
<StatusDot status={asset.status} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="player-overlay-markers">
|
<div className="player-overlay-markers">
|
||||||
{visibleComments
|
{visibleComments
|
||||||
.filter(c => Math.abs(parseDuration(c.time) - currentMs) < 200)
|
.filter(c => Math.abs(parseDuration(c.time) - currentMs) < 200)
|
||||||
|
|
@ -90,26 +113,31 @@ function AssetDetail({ asset, onClose }) {
|
||||||
<span className="badge live">LIVE · REC</span>
|
<span className="badge live">LIVE · REC</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="player-tc">
|
{totalMs > 0 && (
|
||||||
<span className="mono">{msToTimecode(currentMs)}</span>
|
<div className="player-tc">
|
||||||
<span style={{ opacity: 0.5 }}> / {asset.duration}</span>
|
<span className="mono">{msToTimecode(currentMs)}</span>
|
||||||
|
<span style={{ opacity: 0.5 }}> / {asset.duration}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalMs > 0 && (
|
||||||
|
<div className="player-controls">
|
||||||
|
<button className="icon-btn" onClick={() => setPlaying(p => !p)}>
|
||||||
|
<Icon name={playing ? "pause" : "play"} size={14} />
|
||||||
|
</button>
|
||||||
|
<span className="mono" style={{ fontSize: 11.5, color: "var(--text-2)", minWidth: 70 }}>{msToTimecode(currentMs)}</span>
|
||||||
|
<PlaybackBar current={currentMs} total={totalMs} onSeek={seek} comments={visibleComments} />
|
||||||
|
<span className="mono" style={{ fontSize: 11.5, color: "var(--text-3)", minWidth: 70, textAlign: "right" }}>{asset.duration}</span>
|
||||||
|
<div style={{ width: 1, height: 18, background: "var(--border-strong)" }} />
|
||||||
|
<button className="icon-btn"><Icon name="audio" size={14} /></button>
|
||||||
|
<button className="icon-btn"><Icon name="layout" size={14} /></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="player-controls">
|
{totalMs > 0 && (
|
||||||
<button className="icon-btn" onClick={() => setPlaying(p => !p)}>
|
<FilmStrip seed={asset.seed || 1} current={currentMs} total={totalMs} onSeek={seek} comments={visibleComments} />
|
||||||
<Icon name={playing ? "pause" : "play"} size={14} />
|
)}
|
||||||
</button>
|
|
||||||
<span className="mono" style={{ fontSize: 11.5, color: "var(--text-2)", minWidth: 70 }}>{msToTimecode(currentMs)}</span>
|
|
||||||
<PlaybackBar current={currentMs} total={totalMs} onSeek={seek} comments={visibleComments} />
|
|
||||||
<span className="mono" style={{ fontSize: 11.5, color: "var(--text-3)", minWidth: 70, textAlign: "right" }}>{asset.duration}</span>
|
|
||||||
<div style={{ width: 1, height: 18, background: "var(--border-strong)" }} />
|
|
||||||
<button className="icon-btn"><Icon name="audio" size={14} /></button>
|
|
||||||
<button className="icon-btn"><Icon name="layout" size={14} /></button>
|
|
||||||
<button className="icon-btn"><Icon name="externalLink" size={14} /></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FilmStrip seed={asset.seed || 1} current={currentMs} total={totalMs} onSeek={seek} comments={visibleComments} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="asset-detail-tabs">
|
<div className="asset-detail-tabs">
|
||||||
|
|
@ -117,7 +145,7 @@ function AssetDetail({ asset, onClose }) {
|
||||||
Comments <span className="count">{comments.length}</span>
|
Comments <span className="count">{comments.length}</span>
|
||||||
</button>
|
</button>
|
||||||
<button className={tab === "versions" ? "active" : ""} onClick={() => setTab("versions")}>
|
<button className={tab === "versions" ? "active" : ""} onClick={() => setTab("versions")}>
|
||||||
Versions <span className="count">3</span>
|
Versions
|
||||||
</button>
|
</button>
|
||||||
<button className={tab === "metadata" ? "active" : ""} onClick={() => setTab("metadata")}>
|
<button className={tab === "metadata" ? "active" : ""} onClick={() => setTab("metadata")}>
|
||||||
Metadata
|
Metadata
|
||||||
|
|
@ -125,9 +153,6 @@ function AssetDetail({ asset, onClose }) {
|
||||||
<button className={tab === "audio" ? "active" : ""} onClick={() => setTab("audio")}>
|
<button className={tab === "audio" ? "active" : ""} onClick={() => setTab("audio")}>
|
||||||
Audio
|
Audio
|
||||||
</button>
|
</button>
|
||||||
<button className={tab === "activity" ? "active" : ""} onClick={() => setTab("activity")}>
|
|
||||||
Activity
|
|
||||||
</button>
|
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
{tab === "comments" && (
|
{tab === "comments" && (
|
||||||
<label className="tiny-toggle">
|
<label className="tiny-toggle">
|
||||||
|
|
@ -145,7 +170,6 @@ function AssetDetail({ asset, onClose }) {
|
||||||
{tab === "versions" && <VersionsTab />}
|
{tab === "versions" && <VersionsTab />}
|
||||||
{tab === "metadata" && <MetadataTab asset={asset} />}
|
{tab === "metadata" && <MetadataTab asset={asset} />}
|
||||||
{tab === "audio" && <AudioTab asset={asset} />}
|
{tab === "audio" && <AudioTab asset={asset} />}
|
||||||
{tab === "activity" && <AssetActivityTab />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -170,20 +194,21 @@ function PlaybackBar({ current, total, onSeek, comments }) {
|
||||||
const p = (e.clientX - r.left) / r.width;
|
const p = (e.clientX - r.left) / r.width;
|
||||||
onSeek(Math.max(0, Math.min(1, p)) * total);
|
onSeek(Math.max(0, Math.min(1, p)) * total);
|
||||||
};
|
};
|
||||||
const pct = (current / total) * 100;
|
const pct = total > 0 ? (current / total) * 100 : 0;
|
||||||
return (
|
return (
|
||||||
<div className="playback-bar" ref={ref} onClick={handle}>
|
<div className="playback-bar" ref={ref} onClick={handle}>
|
||||||
<div className="playback-fill" style={{ width: `${pct}%` }} />
|
<div className="playback-fill" style={{ width: `${pct}%` }} />
|
||||||
<div className="playback-handle" style={{ left: `${pct}%` }} />
|
<div className="playback-handle" style={{ left: `${pct}%` }} />
|
||||||
{comments.map(c => {
|
{comments.map(c => {
|
||||||
const x = (parseDuration(c.time) / total) * 100;
|
const ct = parseDuration(c.time);
|
||||||
|
if (!ct || total <= 0) return null;
|
||||||
|
const x = (ct / total) * 100;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={c.id}
|
key={c.id}
|
||||||
className={`playback-marker ${c.resolved ? "resolved" : ""}`}
|
className={`playback-marker ${c.resolved ? "resolved" : ""}`}
|
||||||
style={{ left: `${x}%` }}
|
style={{ left: `${x}%` }}
|
||||||
data-tip={`${c.who}: ${c.text.slice(0, 40)}…`}
|
onClick={(e) => { e.stopPropagation(); onSeek(ct); }}
|
||||||
onClick={(e) => { e.stopPropagation(); onSeek(parseDuration(c.time)); }}
|
|
||||||
>
|
>
|
||||||
<span className="marker-avatar">{c.avatar}</span>
|
<span className="marker-avatar">{c.avatar}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -197,23 +222,26 @@ function FilmStrip({ seed, current, total, onSeek, comments }) {
|
||||||
const ref = React.useRef(null);
|
const ref = React.useRef(null);
|
||||||
const frames = 28;
|
const frames = 28;
|
||||||
const handle = (e) => {
|
const handle = (e) => {
|
||||||
|
if (!ref.current || total <= 0) return;
|
||||||
const r = ref.current.getBoundingClientRect();
|
const r = ref.current.getBoundingClientRect();
|
||||||
onSeek(((e.clientX - r.left) / r.width) * total);
|
onSeek(((e.clientX - r.left) / r.width) * total);
|
||||||
};
|
};
|
||||||
|
const pct = total > 0 ? (current / total) * 100 : 0;
|
||||||
return (
|
return (
|
||||||
<div className="filmstrip-wrap">
|
<div className="filmstrip-wrap">
|
||||||
<div className="filmstrip" ref={ref} onClick={handle}>
|
<div className="filmstrip" ref={ref} onClick={handle}>
|
||||||
{Array.from({ length: frames }).map((_, i) => (
|
{Array.from({ length: frames }).map((_, i) => (
|
||||||
<div key={i} className="film-frame" style={{ background: window.ZAMPP_DATA.thumbGrad((seed + i) % 11) }}>
|
<div key={i} className="film-frame" style={{ background: _FRAME_GRADIENTS[(seed + i) % _FRAME_GRADIENTS.length] }}>
|
||||||
<FauxFrame seed={(seed + i) % 6} />
|
<FauxFrame seed={(seed + i) % 6} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="filmstrip-playhead" style={{ left: `${(current / total) * 100}%` }} />
|
<div className="filmstrip-playhead" style={{ left: `${pct}%` }} />
|
||||||
{comments.map(c => {
|
{comments.map(c => {
|
||||||
const x = (parseDuration(c.time) / total) * 100;
|
const ct = parseDuration(c.time);
|
||||||
|
if (!ct || total <= 0) return null;
|
||||||
|
const x = (ct / total) * 100;
|
||||||
return (
|
return (
|
||||||
<div key={c.id} className={`filmstrip-pin ${c.resolved ? "resolved" : ""}`} style={{ left: `${x}%` }}
|
<div key={c.id} className={`filmstrip-pin ${c.resolved ? "resolved" : ""}`} style={{ left: `${x}%` }}>
|
||||||
data-tip={`${c.who} @ ${c.time}`}>
|
|
||||||
<span className="filmstrip-pin-avatar">{c.avatar}</span>
|
<span className="filmstrip-pin-avatar">{c.avatar}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -243,7 +271,7 @@ function CommentsList({ comments, onSeek, onResolve }) {
|
||||||
<button className="comment-time" onClick={() => onSeek(c)}><Icon name="clock" size={10} />{c.time}</button>
|
<button className="comment-time" onClick={() => onSeek(c)}><Icon name="clock" size={10} />{c.time}</button>
|
||||||
<span className="comment-when">{c.real}</span>
|
<span className="comment-when">{c.real}</span>
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
<button className="comment-action" data-tip={c.resolved ? "Reopen" : "Resolve"} onClick={() => onResolve(c.id)}>
|
<button className="comment-action" onClick={() => onResolve(c.id)}>
|
||||||
<Icon name={c.resolved ? "refresh" : "check"} size={12} />
|
<Icon name={c.resolved ? "refresh" : "check"} size={12} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -262,15 +290,14 @@ function CommentComposer({ asset, currentMs, value, onChange, onSubmit }) {
|
||||||
<div className="composer-head">
|
<div className="composer-head">
|
||||||
<span style={{ fontWeight: 600, fontSize: 12 }}>Reviewers</span>
|
<span style={{ fontWeight: 600, fontSize: 12 }}>Reviewers</span>
|
||||||
<div className="avatar-stack">
|
<div className="avatar-stack">
|
||||||
{["KM", "ZG", "MO", "JT"].map((a, i) => (
|
{["ZG"].map((a, i) => (
|
||||||
<div key={i} className="avatar" style={{ background: avatarColor(a), width: 22, height: 22, fontSize: 10 }}>{a}</div>
|
<div key={i} className="avatar" style={{ background: avatarColor(a), width: 22, height: 22, fontSize: 10 }}>{a}</div>
|
||||||
))}
|
))}
|
||||||
<button className="add-reviewer" data-tip="Add reviewer"><Icon name="plus" size={11} /></button>
|
<button className="add-reviewer"><Icon name="plus" size={11} /></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="composer-tag">
|
<div className="composer-tag">
|
||||||
<span className="badge accent">@ {msToTimecode(currentMs)}</span>
|
<span className="badge accent">@ {msToTimecode(currentMs)}</span>
|
||||||
<span style={{ marginLeft: "auto", fontSize: 11, color: "var(--text-3)" }}>Drawing tools <Icon name="editor" size={10} /></span>
|
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
className="composer-input"
|
className="composer-input"
|
||||||
|
|
@ -280,11 +307,6 @@ function CommentComposer({ asset, currentMs, value, onChange, onSubmit }) {
|
||||||
onKeyDown={e => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) onSubmit(); }}
|
onKeyDown={e => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) onSubmit(); }}
|
||||||
/>
|
/>
|
||||||
<div className="composer-bar">
|
<div className="composer-bar">
|
||||||
<div className="composer-tools">
|
|
||||||
<button className="icon-btn"><Icon name="image" size={13} /></button>
|
|
||||||
<button className="icon-btn"><Icon name="link" size={13} /></button>
|
|
||||||
<button className="icon-btn"><Icon name="layers" size={13} /></button>
|
|
||||||
</div>
|
|
||||||
<span style={{ flex: 1 }} />
|
<span style={{ flex: 1 }} />
|
||||||
<span style={{ fontSize: 10.5, color: "var(--text-3)" }}>⌘↵ to send</span>
|
<span style={{ fontSize: 10.5, color: "var(--text-3)" }}>⌘↵ to send</span>
|
||||||
<button className="btn primary sm" onClick={onSubmit} disabled={!value.trim()}>Comment</button>
|
<button className="btn primary sm" onClick={onSubmit} disabled={!value.trim()}>Comment</button>
|
||||||
|
|
@ -294,45 +316,23 @@ function CommentComposer({ asset, currentMs, value, onChange, onSubmit }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function VersionsTab() {
|
function VersionsTab() {
|
||||||
const versions = [
|
|
||||||
{ v: "v3", current: true, by: "Kira Morales", when: "1h ago", note: "Color graded for sponsor compliance" },
|
|
||||||
{ v: "v2", by: "Zach Gaetano", when: "yesterday", note: "Updated lower thirds, fixed audio dip" },
|
|
||||||
{ v: "v1", by: "Jules Tran", when: "2d ago", note: "Initial edit from Stage_Cam_A" },
|
|
||||||
];
|
|
||||||
return (
|
return (
|
||||||
<div className="versions-list">
|
<div style={{ padding: 24, textAlign: 'center', color: 'var(--text-3)', fontSize: 12.5 }}>
|
||||||
{versions.map(v => (
|
Version history not yet available.
|
||||||
<div key={v.v} className="version-row">
|
|
||||||
<div className="version-thumb"><FauxFrame seed={Math.random() * 6 | 0} /></div>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
||||||
<span style={{ fontWeight: 600 }}>{v.v}</span>
|
|
||||||
{v.current && <span className="badge success">Current</span>}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 11.5, color: "var(--text-3)", marginTop: 2 }}>{v.by} · {v.when}</div>
|
|
||||||
<div style={{ fontSize: 12.5, marginTop: 4, color: "var(--text-2)" }}>{v.note}</div>
|
|
||||||
</div>
|
|
||||||
<button className="btn ghost sm">Compare</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MetadataTab({ asset }) {
|
function MetadataTab({ asset }) {
|
||||||
const rows = [
|
const rows = [
|
||||||
{ k: "Filename", v: asset.name },
|
{ k: "Filename", v: asset.name },
|
||||||
{ k: "Duration", v: asset.duration },
|
{ k: "Duration", v: asset.duration || '—' },
|
||||||
{ k: "Resolution", v: asset.res },
|
{ k: "Resolution", v: asset.res || '—' },
|
||||||
{ k: "Frame rate", v: `${asset.fps} fps` },
|
{ k: "Codec", v: asset.codec || '—' },
|
||||||
{ k: "Codec", v: asset.codec },
|
{ k: "File size", v: asset.size || '—' },
|
||||||
{ k: "File size", v: asset.size },
|
{ k: "Status", v: asset.status || '—' },
|
||||||
{ k: "Color space", v: "Rec. 709" },
|
{ k: "Updated", v: asset.updated || '—' },
|
||||||
{ k: "Bit depth", v: "10-bit" },
|
{ k: "Project", v: asset.project || '—' },
|
||||||
{ k: "Audio channels", v: "2.0 stereo" },
|
|
||||||
{ k: "Audio sample rate", v: "48 kHz" },
|
|
||||||
{ k: "Storage path", v: "s3://dragonmam/projects/protour/stage_a/master_001.mov" },
|
|
||||||
{ k: "Checksum (xxh3)", v: "f4a8c2e9b15d3a07" },
|
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="meta-table">
|
<div className="meta-table">
|
||||||
|
|
@ -349,51 +349,35 @@ function MetadataTab({ asset }) {
|
||||||
function AudioTab({ asset }) {
|
function AudioTab({ asset }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: "16px 0" }}>
|
<div style={{ padding: "16px 0" }}>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16 }}>
|
|
||||||
<span style={{ fontSize: 12, fontWeight: 600 }}>Stereo Mix</span>
|
|
||||||
<span className="badge neutral">L</span>
|
|
||||||
<span className="badge neutral">R</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ background: "var(--bg-2)", border: "1px solid var(--border)", borderRadius: 8, padding: 16, display: "flex", flexDirection: "column", gap: 12 }}>
|
<div style={{ background: "var(--bg-2)", border: "1px solid var(--border)", borderRadius: 8, padding: 16, display: "flex", flexDirection: "column", gap: 12 }}>
|
||||||
<Waveform seed={3} color="var(--accent)" />
|
<Waveform seed={3} color="var(--accent)" />
|
||||||
<Waveform seed={7} color="var(--purple)" />
|
<Waveform seed={7} color="var(--purple)" />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 16, fontSize: 12, color: "var(--text-3)" }}>
|
|
||||||
Loudness <span style={{ color: "var(--text-1)", marginLeft: 8 }}>-14.2 LUFS</span>
|
|
||||||
<span style={{ marginLeft: 24 }}>True peak <span style={{ color: "var(--text-1)", marginLeft: 8 }}>-1.4 dBTP</span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AssetActivityTab() {
|
|
||||||
return (
|
|
||||||
<div className="activity-feed" style={{ border: 0, background: "transparent" }}>
|
|
||||||
{window.ZAMPP_DATA.ACTIVITY.slice(0, 6).map(a => (
|
|
||||||
<div key={a.id} className="activity-row" style={{ paddingLeft: 0, paddingRight: 0 }}>
|
|
||||||
<div className={`activity-icon ${a.kind}`}><Icon name="clock" size={12} /></div>
|
|
||||||
<div className="activity-text"><strong>{a.who}</strong> {a.what} <span className="target">{a.target}</span></div>
|
|
||||||
<div className="activity-time">{a.time}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDuration(d) {
|
function parseDuration(d) {
|
||||||
const [h, m, s] = d.split(":").map(Number);
|
if (!d || d === '—' || typeof d !== 'string') return 0;
|
||||||
return ((h * 60 + m) * 60 + s) * 1000;
|
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) {
|
function msToTimecode(ms) {
|
||||||
const total = Math.floor(ms / 1000);
|
const total = Math.floor((ms || 0) / 1000);
|
||||||
const h = Math.floor(total / 3600);
|
const h = Math.floor(total / 3600);
|
||||||
const m = Math.floor((total % 3600) / 60);
|
const m = Math.floor((total % 3600) / 60);
|
||||||
const s = total % 60;
|
const s = total % 60;
|
||||||
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function avatarColor(initials) {
|
function avatarColor(initials) {
|
||||||
let h = 0;
|
let h = 0;
|
||||||
for (const c of initials) h = (h * 31 + c.charCodeAt(0)) & 0xff;
|
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%))`;
|
return `linear-gradient(135deg, hsl(${h % 360} 60% 50%), hsl(${(h + 60) % 360} 60% 45%))`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue