From 4f98f2b7735d7187a1c8cf32dc0e4d100f1f1e6d Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Tue, 26 May 2026 16:07:33 +0000 Subject: [PATCH] feat(asset): filmstrip right-click menu + Files tab Filmstrip: - Right-click on the filmstrip opens a context menu with 'Re-generate filmstrip' and 'Re-generate proxy' - filmstripKey state forces the build effect to re-run on demand without waiting for a streamUrl/totalMs change - Context menu dismisses on click, contextmenu, and scroll Files tab (replaces empty Versions tab): - Proxy: status badge, S3 key path, inline video preview, re-generate button - Hi-res master: status badge and S3 key path - Thumbnail: status badge, S3 key path, inline thumbnail image, re-generate button - Filmstrip: status badge, frame count, scrollable strip of first 14 frames, re-generate button (disabled while building) --- services/web-ui/public/screens-asset.jsx | 187 +++++++++++++++++++++-- 1 file changed, 177 insertions(+), 10 deletions(-) diff --git a/services/web-ui/public/screens-asset.jsx b/services/web-ui/public/screens-asset.jsx index 833dfd7..c963341 100644 --- a/services/web-ui/public/screens-asset.jsx +++ b/services/web-ui/public/screens-asset.jsx @@ -37,6 +37,7 @@ function AssetDetail({ asset, onClose }) { const [reprocessing, setReprocessing] = React.useState(null); // 'proxy' | 'thumbnail' | null const [filmFrames, setFilmFrames] = React.useState([]); const [filmstripLoading, setFilmstripLoading] = React.useState(false); + const [filmstripKey, setFilmstripKey] = React.useState(0); const videoRef = React.useRef(null); const assetId = asset && asset.id; @@ -164,7 +165,7 @@ function AssetDetail({ asset, onClose }) { }; build(); return function() { cancelled = true; }; - }, [streamUrl, streamType, totalMs]); + }, [streamUrl, streamType, totalMs, filmstripKey]); // Fake playback timer — only used when no real video stream React.useEffect(() => { @@ -491,7 +492,18 @@ function AssetDetail({ asset, onClose }) { )} {totalMs > 0 && ( - + )} @@ -499,8 +511,8 @@ function AssetDetail({ asset, onClose }) { - + + + )} ); } @@ -696,10 +751,122 @@ function CommentComposer({ asset, currentMs, value, onChange, onSubmit }) { ); } -function VersionsTab() { +function FilesTab({ asset, filmFrames, filmstripLoading, streamUrl, reprocessing, onRegenProxy, onRegenThumbnail, onRegenFilmstrip }) { + const hasProxy = !!asset.proxy_s3_key; + const hasHires = !!asset.original_s3_key; + const hasThumb = !!asset.thumbnail_s3_key; + const hasFilmstrip = Array.isArray(filmFrames) && filmFrames.length > 0; + + // Rows: label | status badge | path | action button + const FileRow = function({ label, present, path, icon, actionLabel, onAction, disabled, children }) { + return ( +
+
+ +
+
+ {label} + {present + ? ready + : missing} +
+ {path && ( +
+ {path} +
+ )} +
+ {actionLabel && onAction && ( + + )} +
+ {children &&
{children}
} +
+ ); + }; + return ( -
- Version history not yet available. +
+ + {/* Proxy */} + + {streamUrl && ( + + + {/* Hi-res */} + + + {/* Thumbnail */} + + {hasThumb && ( + Thumbnail + )} + + + {/* Filmstrip */} + + {hasFilmstrip && ( +
+ {filmFrames.filter(Boolean).slice(0, 14).map(function(src, i) { + return ( + + ); + })} +
+ )} + {filmstripLoading && ( +
Building filmstrip from proxy…
+ )} +
+
); }