Add multi-select to library page
- Selection mode toggle in toolbar - Checkboxes on cards (grid) and rows (list) - Bulk actions: move to bin, delete - Select all / clear selection controls Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2cd20a0e72
commit
9bcbac558c
1 changed files with 104 additions and 4 deletions
|
|
@ -77,6 +77,9 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
|||
// Rename project state
|
||||
const [renamingProject, setRenamingProject] = React.useState(null);
|
||||
const [projVersion, setProjVersion] = React.useState(0);
|
||||
// Multi-select state
|
||||
const [selectedAssets, setSelectedAssets] = React.useState(new Set());
|
||||
const [selectionMode, setSelectionMode] = React.useState(false);
|
||||
|
||||
const refreshAssets = React.useCallback(() => {
|
||||
window.ZAMPP_API.refreshAssets()
|
||||
|
|
@ -86,6 +89,66 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
|||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const toggleSelection = function(assetId, e) {
|
||||
if (e) e.stopPropagation();
|
||||
setSelectedAssets(function(prev) {
|
||||
var next = new Set(prev);
|
||||
if (next.has(assetId)) next.delete(assetId);
|
||||
else next.add(assetId);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const selectAll = function() {
|
||||
setSelectedAssets(new Set(assets.map(function(a) { return a.id; })));
|
||||
};
|
||||
|
||||
const clearSelection = function() {
|
||||
setSelectedAssets(new Set());
|
||||
setSelectionMode(false);
|
||||
};
|
||||
|
||||
const bulkMoveToBin = async function(binId) {
|
||||
var ids = Array.from(selectedAssets);
|
||||
if (ids.length === 0) return;
|
||||
var targetBin = bins.find(function(b) { return b.id === binId; });
|
||||
for (var i = 0; i < ids.length; i++) {
|
||||
var asset = allAssets.find(function(a) { return a.id === ids[i]; });
|
||||
if (asset && targetBin && asset.project_id && targetBin.project_id && asset.project_id !== targetBin.project_id) {
|
||||
alert('Cannot move assets to a bin in a different project.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await Promise.all(ids.map(function(id) {
|
||||
return window.ZAMPP_API.fetch('/assets/' + id, { method: 'PATCH', body: JSON.stringify({ bin_id: binId }) });
|
||||
}));
|
||||
refreshAssets();
|
||||
window.dispatchEvent(new Event('df:bins-changed'));
|
||||
clearSelection();
|
||||
} catch (e) {
|
||||
alert('Bulk move failed: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const bulkDelete = async function() {
|
||||
var ids = Array.from(selectedAssets);
|
||||
if (ids.length === 0) return;
|
||||
if (!(await confirm({
|
||||
title: 'Delete ' + ids.length + ' assets?',
|
||||
message: 'Delete ' + ids.length + ' assets permanently?\nThis removes the database rows and S3 objects.\nThis cannot be undone.',
|
||||
}))) return;
|
||||
try {
|
||||
await Promise.all(ids.map(function(id) {
|
||||
return window.ZAMPP_API.fetch('/assets/' + id + '?hard=true', { method: 'DELETE' });
|
||||
}));
|
||||
refreshAssets();
|
||||
clearSelection();
|
||||
} catch (e) {
|
||||
alert('Bulk delete failed: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAsset = React.useCallback(async (asset) => {
|
||||
if (!(await confirm({
|
||||
title: 'Delete asset?',
|
||||
|
|
@ -322,6 +385,29 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
|||
<h1 className="toolbar-title">{displayTitle}</h1>
|
||||
<span className="count">· {assets.length} assets</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
{selectionMode && selectedAssets.size > 0 && (
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginRight: 12 }}>
|
||||
<span style={{ fontSize: 13, color: 'var(--text-2)' }}>{selectedAssets.size} selected</span>
|
||||
<div className="tab-group">
|
||||
<button onClick={selectAll} title="Select all"><Icon name="check" size={12} /></button>
|
||||
<button onClick={clearSelection} title="Clear selection"><Icon name="x" size={12} /></button>
|
||||
</div>
|
||||
{BINS.length > 0 && (
|
||||
<select className="field-input" style={{ height: 32, fontSize: 12, padding: '0 8px' }}
|
||||
onChange={function(e) { if (e.target.value) bulkMoveToBin(e.target.value); e.target.value = ''; }}
|
||||
value="">
|
||||
<option value="">Move to bin…</option>
|
||||
{BINS.filter(function(b) { return !openProject || b.project_id === openProject.id; }).map(function(b) {
|
||||
return <option key={b.id} value={b.id}>{b.name}</option>;
|
||||
})}
|
||||
</select>
|
||||
)}
|
||||
<button className="btn danger sm" onClick={bulkDelete}><Icon name="trash" size={12} />Delete</button>
|
||||
</div>
|
||||
)}
|
||||
<button className={'btn ghost sm' + (selectionMode ? ' active' : '')} onClick={function() { setSelectionMode(!selectionMode); if (selectionMode) clearSelection(); }} title="Select multiple">
|
||||
<Icon name="check" size={12} />Select
|
||||
</button>
|
||||
<div className="search" style={{ width: 220 }}>
|
||||
<Icon name="search" className="search-icon" />
|
||||
<input value={search} onChange={function(e) { setSearch(e.target.value); }} placeholder="Filter assets…" />
|
||||
|
|
@ -352,18 +438,27 @@ function Library({ navigate, onOpenAsset, openProject, onClearProject }) {
|
|||
onContextMenu={function(e) { openCtx(a, e); }}
|
||||
onDownload={function() { requestDownload(a); }}
|
||||
onDragStart={function(e) { onAssetDragStart(a.id, e); }}
|
||||
draggable={true} />;
|
||||
draggable={true}
|
||||
selectionMode={selectionMode}
|
||||
isSelected={selectedAssets.has(a.id)}
|
||||
onToggleSelect={function(e) { toggleSelection(a.id, e); }} />;
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="library-list">
|
||||
<div className="list-row head">
|
||||
{selectionMode && <div style={{ width: 32 }}></div>}
|
||||
<div></div><div>Name</div><div>Duration</div><div>Resolution</div><div>Codec</div><div>Size</div><div>Updated</div><div></div>
|
||||
</div>
|
||||
{assets.map(function(a) {
|
||||
return (
|
||||
<div key={a.id} className="list-row" onClick={function() { onOpenAsset(a); }} onContextMenu={function(e) { openCtx(a, e); }} style={{ cursor: 'pointer' }}
|
||||
<div key={a.id} className="list-row" onClick={function() { if (!selectionMode) onOpenAsset(a); }} onContextMenu={function(e) { openCtx(a, e); }} style={{ cursor: 'pointer' }}
|
||||
draggable="true" onDragStart={function(e) { onAssetDragStart(a.id, e); }}>
|
||||
{selectionMode && (
|
||||
<div style={{ width: 32, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<input type="checkbox" checked={selectedAssets.has(a.id)} onChange={function(e) { toggleSelection(a.id, e); }} onClick={function(e) { e.stopPropagation(); }} style={{ cursor: 'pointer' }} />
|
||||
</div>
|
||||
)}
|
||||
<div className="thumb"><AssetThumb asset={a} /></div>
|
||||
<div>
|
||||
<div className="name">{a.name}</div>
|
||||
|
|
@ -555,7 +650,7 @@ function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen, onRen
|
|||
);
|
||||
}
|
||||
|
||||
function AssetCard({ asset, onOpen, onContextMenu, onDownload, onDragStart, draggable }) {
|
||||
function AssetCard({ asset, onOpen, onContextMenu, onDownload, onDragStart, draggable, selectionMode, isSelected, onToggleSelect }) {
|
||||
const [hoverStream, setHoverStream] = React.useState(null);
|
||||
const [hovered, setHovered] = React.useState(false);
|
||||
const timerRef = React.useRef(null);
|
||||
|
|
@ -593,9 +688,14 @@ function AssetCard({ asset, onOpen, onContextMenu, onDownload, onDragStart, drag
|
|||
const showVideo = hovered && hoverStream;
|
||||
|
||||
return (
|
||||
<div className="asset-card" onClick={onOpen} onContextMenu={onContextMenu} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}
|
||||
<div className="asset-card" onClick={selectionMode ? onToggleSelect : onOpen} onContextMenu={onContextMenu} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}
|
||||
draggable={draggable} onDragStart={onDragStart}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
{selectionMode && (
|
||||
<div style={{ position: 'absolute', top: 8, left: 8, zIndex: 10 }}>
|
||||
<input type="checkbox" checked={isSelected} onChange={onToggleSelect} onClick={function(e) { e.stopPropagation(); }} style={{ cursor: 'pointer', width: 18, height: 18 }} />
|
||||
</div>
|
||||
)}
|
||||
<AssetThumb asset={asset} />
|
||||
{showVideo && (
|
||||
<video
|
||||
|
|
|
|||
Loading…
Reference in a new issue