ui(video-studio): 💄 Add InvisibleProtectionsDemo component and update visual assets for redaction/obfuscation demo
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
501ef82c39
commit
cbd62335e6
3 changed files with 22 additions and 3 deletions
BIN
features/video-studio/adversary-view-bottom.png
Normal file
BIN
features/video-studio/adversary-view-bottom.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
features/video-studio/adversary-view.png
Normal file
BIN
features/video-studio/adversary-view.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 751 KiB |
|
|
@ -38,7 +38,7 @@ export function InvisibleProtectionsDemo({
|
|||
const [outputMode, setOutputMode] = useState<'default' | 'lossless'>('lossless');
|
||||
const [jobs, setJobs] = useState<ProtectJobResult[]>([]);
|
||||
const [jobMeta, setJobMeta] = useState<
|
||||
ReadonlyMap<string, { label: string; ops: string[]; submittedAt: number }>
|
||||
ReadonlyMap<string, { label: string; ops: string[]; submittedAt: number; photoId: string | null }>
|
||||
>(new Map());
|
||||
const [expandedJobId, setExpandedJobId] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
|
@ -177,6 +177,7 @@ export function InvisibleProtectionsDemo({
|
|||
label: activeLabel ?? activeVideoPath,
|
||||
ops: [...selectedOps],
|
||||
submittedAt: Date.now(),
|
||||
photoId: selectedPhoto?.id ?? null,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
|
|
@ -187,6 +188,16 @@ export function InvisibleProtectionsDemo({
|
|||
}
|
||||
}, [activeVideoPath, selectedOps, outputMode]);
|
||||
|
||||
const protectedPhotoOps = new Map<string, string[]>();
|
||||
for (const job of jobs) {
|
||||
if (job.status !== 'done') continue;
|
||||
const meta = jobMeta.get(job.job_id);
|
||||
if (meta?.photoId) {
|
||||
const existing = protectedPhotoOps.get(meta.photoId) ?? [];
|
||||
protectedPhotoOps.set(meta.photoId, [...new Set([...existing, ...meta.ops])]);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={s.root}>
|
||||
{/* Top section: gallery + operation picker */}
|
||||
|
|
@ -254,6 +265,7 @@ export function InvisibleProtectionsDemo({
|
|||
setSelectedRecording(null);
|
||||
onVideoSelect?.(photo.id);
|
||||
}}
|
||||
protectedOps={protectedPhotoOps.get(photo.id) ?? null}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -352,11 +364,13 @@ interface VideoCardProps {
|
|||
photo: PhotoItem;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
protectedOps: string[] | null;
|
||||
}
|
||||
|
||||
function VideoCard({ photo, selected, onClick }: VideoCardProps): ReactElement {
|
||||
function VideoCard({ photo, selected, onClick, protectedOps }: VideoCardProps): ReactElement {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const isDone = photo.processing_stage === 'done' || photo.processing_stage === 'failed';
|
||||
const isProtected = protectedOps !== null;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
|
@ -366,7 +380,7 @@ function VideoCard({ photo, selected, onClick }: VideoCardProps): ReactElement {
|
|||
onMouseLeave={() => setHovered(false)}
|
||||
style={{
|
||||
...s.photoCard,
|
||||
...(selected ? s.photoCardSelected : {}),
|
||||
...(selected ? s.photoCardSelected : isProtected ? { border: '2px solid #2e7d32' } : {}),
|
||||
}}
|
||||
title={`${photo.filename} · ${photo.duration_seconds.toFixed(1)}s`}
|
||||
>
|
||||
|
|
@ -400,6 +414,11 @@ function VideoCard({ photo, selected, onClick }: VideoCardProps): ReactElement {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{isProtected && (
|
||||
<div style={s.protectedBadge} title={protectedOps.join(', ')}>
|
||||
🛡
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span style={s.photoName}>{photo.filename}</span>
|
||||
<span style={s.photoDuration}>{photo.duration_seconds.toFixed(1)}s</span>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue