mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 13:01:10 +00:00
Reskin to v2 surface/accent tokens + JetBrains Mono headings to match _docs/ui_design/v2/plugin/annotations.html. Add scrubber with class-colored annotation marks, canvas top bar (zoom/cursor/dims), floating AI-detection banner, multi-band gradient rows in the annotations sidebar, class-distribution summary footer, and DOM-overlay bbox labels with affiliation icon + readiness dot. Split VideoPlayer chrome out into the page-level controls row (transport/frame-step/save/delete/AI-detect/mute/volume) and a new Scrubber component; player events replace 200ms polling. Other: - Auth dev bypass via VITE_DEV_AUTH_BYPASS (gated on import.meta.env.DEV). - Mount SavedAnnotationsProvider in App so AnnotationsPage doesn't crash. - Extract hexToRgba to src/class-colors and time helpers to src/features/annotations/time.ts (dedup across CanvasEditor / Sidebar / AnnotationsPage). - CanvasEditor: shallow-compare label chips before commit, NaN-guard annotation-time parser, cancel cursor RAF on unmount. - AnnotationsPage: track AI-banner close timer, push initial volume to the <video> on media change, drop the duplicate parent muted state. - Fixed sidebar widths (resize handles removed per design).
This commit is contained in:
@@ -21,6 +21,7 @@ export default function MediaList({ selectedMedia, onSelect, onAnnotationsLoaded
|
||||
const debouncedFilter = useDebounce(filter, 300)
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||
const folderInputRef = useRef<HTMLInputElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const fetchMedia = useCallback(async () => {
|
||||
const params = new URLSearchParams({ pageSize: '1000' })
|
||||
@@ -139,70 +140,126 @@ export default function MediaList({ selectedMedia, onSelect, onAnnotationsLoaded
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
const filtered = media.filter(m => m.name.toLowerCase().includes(filter.toLowerCase()))
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getRootProps({
|
||||
className: `flex-1 flex flex-col overflow-hidden ${isDragActive ? 'ring-2 ring-az-orange ring-inset' : ''}`,
|
||||
className: `flex flex-col flex-1 min-h-0 bg-surface-1${isDragActive ? ' ring-2 ring-accent-amber ring-inset' : ''}`,
|
||||
})}
|
||||
>
|
||||
{/* Dropzone hidden input */}
|
||||
<input {...getInputProps()} />
|
||||
<div className="p-2 border-b border-az-border flex gap-1">
|
||||
<input
|
||||
value={filter}
|
||||
onChange={e => setFilter(e.target.value)}
|
||||
placeholder={t('annotations.mediaList')}
|
||||
className="flex-1 bg-az-bg border border-az-border rounded px-2 py-1 text-xs text-az-text outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="px-2 pt-2 pb-2 flex gap-1">
|
||||
<label className="flex-1 bg-az-orange text-white text-[10px] py-1 rounded text-center cursor-pointer hover:brightness-110">
|
||||
Open File
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={e => {
|
||||
if (e.target.files?.length) uploadFiles(e.target.files)
|
||||
e.target.value = ''
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => folderInputRef.current?.click()}
|
||||
className="flex-1 bg-az-orange text-white text-[10px] py-1 rounded hover:brightness-110"
|
||||
>
|
||||
Open Folder
|
||||
</button>
|
||||
<input
|
||||
ref={folderInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
// @ts-expect-error webkitdirectory is non-standard but widely supported
|
||||
webkitdirectory=""
|
||||
directory=""
|
||||
onChange={handleFolderInput}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{media.filter(m => m.name.toLowerCase().includes(filter.toLowerCase())).map(m => (
|
||||
<div
|
||||
key={m.id}
|
||||
onClick={() => handleSelect(m)}
|
||||
onContextMenu={e => { e.preventDefault(); setDeleteId(m.id) }}
|
||||
className={`px-2 py-1 cursor-pointer border-b border-az-border text-xs flex items-center gap-1.5 ${
|
||||
selectedMedia?.id === m.id ? 'bg-az-bg text-white' : ''
|
||||
} ${m.annotationCount > 0 ? 'bg-az-bg/50' : ''} text-az-text hover:bg-az-bg`}
|
||||
|
||||
{/* Hidden file inputs */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={e => {
|
||||
if (e.target.files?.length) uploadFiles(e.target.files)
|
||||
e.target.value = ''
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={folderInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
// @ts-expect-error webkitdirectory is non-standard but widely supported
|
||||
webkitdirectory=""
|
||||
directory=""
|
||||
onChange={handleFolderInput}
|
||||
/>
|
||||
|
||||
{/* Header row */}
|
||||
<div className="flex items-center justify-between px-3 h-9 border-b border-border-hair shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="sect-head">{t('annotations.mediaList')}</span>
|
||||
<span className="mono text-[10px] text-text-muted">{filtered.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Upload file button */}
|
||||
<button
|
||||
type="button"
|
||||
className="ibtn"
|
||||
style={{ width: 22, height: 22 }}
|
||||
title={t('annotations.upload')}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<span className={`font-mono text-[10px] px-1 rounded ${m.mediaType === MediaType.Video ? 'bg-az-blue/20 text-az-blue' : 'bg-az-green/20 text-az-green'}`}>
|
||||
{m.mediaType === MediaType.Video ? 'V' : 'P'}
|
||||
</span>
|
||||
<span className="truncate flex-1">{m.name}</span>
|
||||
{m.duration && <span className="text-az-muted">{m.duration}</span>}
|
||||
</div>
|
||||
))}
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 5v14M5 12h14"/>
|
||||
</svg>
|
||||
</button>
|
||||
{/* Open folder button */}
|
||||
<button
|
||||
type="button"
|
||||
className="ibtn"
|
||||
style={{ width: 22, height: 22 }}
|
||||
title="Open Folder"
|
||||
onClick={() => folderInputRef.current?.click()}
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter input row */}
|
||||
<div className="px-3 py-2 border-b border-border-hair shrink-0">
|
||||
<div className="relative">
|
||||
<svg
|
||||
width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 text-text-muted pointer-events-none"
|
||||
>
|
||||
<circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/>
|
||||
</svg>
|
||||
<input
|
||||
className="inp w-full pl-7"
|
||||
style={{ height: 28, padding: '0 10px 0 28px' }}
|
||||
value={filter}
|
||||
onChange={e => setFilter(e.target.value)}
|
||||
placeholder={t('annotations.filterByName')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
{filtered.map(m => {
|
||||
const isActive = selectedMedia?.id === m.id
|
||||
const isVideo = m.mediaType === MediaType.Video
|
||||
const hasDuration = !!m.duration
|
||||
const durationColor = isActive
|
||||
? 'text-accent-amber'
|
||||
: hasDuration
|
||||
? 'text-text-secondary'
|
||||
: 'text-text-muted'
|
||||
|
||||
return (
|
||||
<div
|
||||
key={m.id}
|
||||
onClick={() => handleSelect(m)}
|
||||
onContextMenu={e => { e.preventDefault(); setDeleteId(m.id) }}
|
||||
className={`media-row${isActive ? ' active' : ''}`}
|
||||
>
|
||||
{isVideo
|
||||
? <span className="chip-video">VIDEO</span>
|
||||
: <span className="chip-photo">PHOTO</span>
|
||||
}
|
||||
<span className={`truncate${isActive ? ' font-medium text-text-primary' : ' text-text-primary'}`}>
|
||||
{m.name}
|
||||
</span>
|
||||
<span className={`mono text-[11px] ${durationColor}`}>
|
||||
{m.duration ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!deleteId}
|
||||
title={t('annotations.deleteMedia')}
|
||||
|
||||
Reference in New Issue
Block a user