diff --git a/services/web-ui/public/screens-library.jsx b/services/web-ui/public/screens-library.jsx index 17a4bc0..1b318d5 100644 --- a/services/web-ui/public/screens-library.jsx +++ b/services/web-ui/public/screens-library.jsx @@ -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 }) {

{displayTitle}

· {assets.length} assets
+ {selectionMode && selectedAssets.size > 0 && ( +
+ {selectedAssets.size} selected +
+ + +
+ {BINS.length > 0 && ( + + )} + +
+ )} +
@@ -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); }} />; })}
) : (
+ {selectionMode &&
}
Name
Duration
Resolution
Codec
Size
Updated
{assets.map(function(a) { return ( -
+ {selectionMode && ( +
+ +
+ )}
{a.name}
@@ -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 ( -
+ {selectionMode && ( +
+ +
+ )} {showVideo && (