Add image similarity grouping
This commit is contained in:
@@ -113,3 +113,17 @@
|
||||
.vs-jobs-compact .vs-task { padding:8px 10px; margin-bottom:6px; }
|
||||
.vs-jobs-compact .vs-task .vs-toolbar { margin-top:4px; }
|
||||
.vs-jobs-compact .vs-task details { margin:4px 0; }
|
||||
|
||||
.vs-image-review button:not(.theia-button) {background:var(--theia-button-secondaryBackground);color:var(--theia-button-secondaryForeground);border:1px solid var(--theia-panel-border);padding:5px 8px;font:inherit;}
|
||||
.vs-image-review .vs-toolbar label {display:flex;align-items:center;gap:6px;margin:0;font-size:12px;}
|
||||
.vs-image-review .vs-toolbar input {width:5em;min-width:0;flex:none;}
|
||||
.vs-image-review input,.vs-image-review select {background:var(--theia-input-background);color:var(--theia-input-foreground);border:1px solid var(--theia-input-border,var(--theia-panel-border));padding:4px;}
|
||||
.vs-image-review .vs-toolbar .theia-button {margin:0;}
|
||||
.vs-image-review .vs-asset>button:first-child {display:block;width:100%;padding:0;background:transparent;border:0;}
|
||||
.vs-image-review .vs-asset {overflow-wrap:anywhere;}
|
||||
.vs-image-review .vs-asset>div {padding:4px 0;}
|
||||
.vs-image-review summary {overflow-wrap:anywhere;}
|
||||
.vs-image-review .vs-image-group img {width:100%;height:140px;object-fit:contain;display:block;}
|
||||
.vs-image-review .vs-image-group strong,.vs-image-review .vs-image-group span {display:block;padding:5px 8px;text-align:left;}
|
||||
.vs-image-review .vs-image-group span {overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--theia-descriptionForeground);}
|
||||
.vs-image-review .vs-image-group button:hover {outline:1px solid var(--theia-focusBorder);}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ImageReview } from './image-review';
|
||||
import { AudioPreview } from './audio-preview';
|
||||
import { ScriptBrowser } from './script-browser';
|
||||
import { FunctionReuse } from './signature-library';
|
||||
@@ -47,16 +48,19 @@ function Assets({state,menus}:{state:WorkbenchState;menus:ContextMenuRenderer}):
|
||||
const revision=useStateChange(state);const options=useViewOptions(state);const [requested,setRequested]=React.useState({revision,page:0}),[result,setResult]=React.useState<Page>(),[loading,setLoading]=React.useState(false),[error,setError]=React.useState('');
|
||||
const page=requested.revision===revision?requested.page:0;const setPage=(value:number)=>setRequested({revision,page:value});
|
||||
React.useEffect(()=>{let alive=true;setLoading(true);setError('');state.files({page}).then(r=>{if(alive){setResult(r);}}).catch(e=>alive&&setError(String(e))).finally(()=>alive&&setLoading(false));return()=>{alive=false;};},[revision,page]);
|
||||
const [imageReview,setImageReview]=React.useState(false);
|
||||
const baseline=state.workspaces.find(w=>w.id===state.baseline);
|
||||
const soundCues=state.current?.sound_cues||[];
|
||||
return <div className="vs-view"><div className="vs-toolbar"><strong>{state.current?releaseLabel(state.current.release):'Open a working tree'}</strong><span className="vs-spacer"/><button className="theia-button secondary" onClick={()=>state.showView.fire('compare')}>Compare releases</button><ViewOptionsButton state={state} menus={menus}/></div>
|
||||
<div className="vs-toolbar"><div className="vs-segments" aria-label="Asset type">{[['','All'],['image','Images'],['video','Videos'],['audio','Audio'],['script','Scripts'],['file','Files']].map(([kind,label])=><button key={kind} aria-pressed={state.kind===kind} onClick={()=>state.set({kind})}>{label}</button>)}</div><input className="theia-input" aria-label="Filter files and assets" placeholder="Filter by name or path" value={state.search} onChange={e=>state.set({search:e.target.value})}/><ChangeFilter state={state}/>{state.kind==='audio'&&<><select aria-label="Sort assets by" className="theia-select" value={state.sort} onChange={e=>state.set({sort:e.target.value})}><option value="name">Name</option><option value="duration">Audio length</option><option value="size">File size</option><option value="path">Path</option></select><button aria-label="Reverse sort order" onClick={()=>state.set({descending:!state.descending})}>{state.descending?'Descending':'Ascending'}</button></>}{state.kind==='audio'&&<select aria-label="Sound category" title="Estimated from verified cue names" value={state.category} onChange={e=>state.set({category:e.target.value})}><option value="">All sounds</option>{['dialogue','system','music','sfx','unknown'].map(c=><option key={c} value={c}>{c==='sfx'?'SFX':c[0].toUpperCase()+c.slice(1)}</option>)}</select>}</div>
|
||||
{state.kind==='image'&&<div className="vs-toolbar"><button aria-pressed={imageReview} onClick={()=>setImageReview(!imageReview)}>{imageReview?'Back to image gallery':'Group similar images'}</button></div>}
|
||||
{state.kind==='image'&&imageReview?<ImageReview key={state.current?.id} state={state}/>:<>
|
||||
<div className="vs-breadcrumb"><button onClick={()=>state.set({prefix:''})}>Working tree</button>{state.prefix.split('/').filter(Boolean).map((part,i)=><React.Fragment key={i}><span>/</span><button onClick={()=>state.set({prefix:state.prefix.split('/').slice(0,i+1).join('/')})}>{part}</button></React.Fragment>)}<span className="vs-spacer"/><span>{result?.total.toLocaleString()||0} items</span></div>
|
||||
{baseline&&<div className="vs-note">Changes from <b>{baseline.release.edition} / {baseline.release.version}</b> → <b>{state.current?.release.edition} / {state.current?.release.version}</b>. Removed items are shown from the older release.{result&&!result.profiles_compatible&&<p>Some extraction coverage differs. Those assets are marked “Coverage differs” and are excluded from Added/Removed claims.</p>}</div>}
|
||||
{error&&<p className="vs-error" role="alert">{error}</p>}
|
||||
{state.kind==='audio'&&!!soundCues.length&&<details className="vs-panel"><summary>Sound cue catalog ({soundCues.length} cues, {soundCues.filter(c=>c.resolved).length} linked to audio)</summary><p>Cues without a verified audio link remain listed in the source script.</p><div className="vs-scroll" style={{maxHeight:240}}>{soundCues.filter(c=>c.name.toLowerCase().includes(state.search.toLowerCase())).map(c=><div key={c.script_path+c.name}>{c.resolved?<button onClick={()=>state.set({search:c.name})}>{c.name}</button>:<span>{c.name} · Audio link unresolved</span>} <a href={fileURL(c.script_snapshot,c.script_path)}>Source</a></div>)}</div></details>}
|
||||
{state.kind==='script'?<ScriptBrowser state={state} assets={result?.items||[]} loading={loading}/>:<div className="vs-scroll" aria-busy={loading}>{loading?<p className="vs-empty" role="status">Loading assets…</p>:!result?.items.length?<div className="vs-empty"><h3>No items match this view</h3><p>Choose another asset type or clear the folder and change filters.</p><button className="theia-button secondary" onClick={()=>state.set({prefix:'',search:'',kind:'',change:''})}>Clear filters</button></div>:options.layout==='grid'?<div className="vs-gallery">{result.items.map(a=><article key={a.path} className="vs-asset" onFocus={()=>state.inspectAsset.fire(a)}><div className="vs-thumbnail">{a.kind==='image'||a.kind==='file'?<button aria-label={`Open ${basename(a.path)}`} onClick={()=>state.openAsset.fire(a)}><Media asset={a} dmdTint={options.dmdTint}/></button>:<Media asset={a} dmdTint={options.dmdTint}/>}</div><button className="vs-asset-name" title={a.path} onClick={()=>state.openAsset.fire(a)}>{a.name||basename(a.path)}</button><div className="vs-asset-caption"><span>{a.kind} · {sizeLabel(a.entry.size)}{a.kind==='audio'&&<> · {a.duration_seconds!=null?`${a.duration_seconds.toFixed(2)} s`:'Duration unknown'} · {a.audio_category&&a.audio_category!=='unknown'?`${a.audio_category} (estimated)`:'Unknown category'}</>}</span>{a.change&&<span className={`vs-change ${a.change}`}>{a.change==='unknown'?'Coverage differs':a.change}</span>}</div>{a.provenance?.preview_accuracy==='approximate_palette'&&<span className="vs-muted">Approximate bitmap</span>}</article>)}</div>:<table className="vs-table"><thead><tr><th>Name / path</th><th>Type</th><th>Size</th><th>Audio length</th><th>Category (estimated)</th><th>Change</th></tr></thead><tbody>{result.items.map(a=><tr key={a.path}><td><button title={a.path} onClick={()=>state.openAsset.fire(a)}>{a.name?`${a.name} · ${a.path}`:a.path}</button></td><td>{a.kind}</td><td>{sizeLabel(a.entry.size)}</td><td>{a.duration_seconds!=null?`${a.duration_seconds.toFixed(2)} s`:'—'}</td><td>{a.kind==='audio'?(a.audio_category||'unknown'):'—'}</td><td className={`vs-change ${a.change}`}>{a.change}</td></tr>)}</tbody></table>}</div>}
|
||||
<div className="vs-footer"><Pagination label="Assets" page={result?.page??page} total={result?.total||0} pageSize={result?.page_size||120} disabled={loading} onPage={setPage}/><span className="vs-spacer"/><span>Double-click files in Explorer to open an editor</span></div></div>;
|
||||
<div className="vs-footer"><Pagination label="Assets" page={result?.page??page} total={result?.total||0} pageSize={result?.page_size||120} disabled={loading} onPage={setPage}/><span className="vs-spacer"/><span>Double-click files in Explorer to open an editor</span></div></>}</div>;
|
||||
}
|
||||
@injectable()
|
||||
export class AssetsWidget extends ReactWidget {
|
||||
|
||||
Reference in New Issue
Block a user