multi slct del
This commit is contained in:
104
src/App.tsx
104
src/App.tsx
@@ -221,6 +221,12 @@ function AppShell({
|
||||
const [status, setStatus] = useState<string>("");
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
const [selectedAssetId, setSelectedAssetId] = useState<number | null>(null);
|
||||
// Ctrl/Cmd+click extends an additive selection that the Delete shortcut acts
|
||||
// on in bulk. Plain click (single-select) clears it so the two paths don't
|
||||
// mix and surprise the user with stale halos.
|
||||
const [multiSelectedIds, setMultiSelectedIds] = useState<Set<number>>(
|
||||
() => new Set(),
|
||||
);
|
||||
const [bulkVideoName, setBulkVideoName] = useState<string>("");
|
||||
const [mergePrimary, setMergePrimary] = useState<Asset | null>(null);
|
||||
const [mergeSecondary, setMergeSecondary] = useState<Asset | null>(null);
|
||||
@@ -551,6 +557,10 @@ function AppShell({
|
||||
}, [autoLinkMaxM]);
|
||||
const [lassoListOpen, setLassoListOpen] = useState<boolean>(false);
|
||||
const [showDeleted, setShowDeleted] = useState<boolean>(false);
|
||||
// "Only deleted" inverts the deleted-asset filter: live rows are hidden and
|
||||
// the bbox fetch implicitly includes deleted rows. Useful for triaging the
|
||||
// trash without scrolling through everything else.
|
||||
const [onlyDeleted, setOnlyDeleted] = useState<boolean>(false);
|
||||
const [showOutOfScope, setShowOutOfScope] = useState<boolean>(true);
|
||||
const [showInScope, setShowInScope] = useState<boolean>(true);
|
||||
const [visibilityOpen, setVisibilityOpen] = useState<boolean>(false);
|
||||
@@ -701,12 +711,15 @@ function AppShell({
|
||||
!filters.videos.has(displayVideo(a.video_name))
|
||||
)
|
||||
return false;
|
||||
if (onlyDeleted) {
|
||||
if (a.deleted === 0) return false;
|
||||
}
|
||||
const inScope = a.in_scope !== 0;
|
||||
if (!showOutOfScope && !inScope) return false;
|
||||
if (!showInScope && inScope) return false;
|
||||
return true;
|
||||
});
|
||||
}, [assets, filters, showOutOfScope, showInScope, allVideoNames]);
|
||||
}, [assets, filters, showOutOfScope, showInScope, onlyDeleted, allVideoNames]);
|
||||
|
||||
// Per-asset_name counts within the current video/side/type filter context
|
||||
// (but ignoring the names filter itself, so toggling a name doesn't make the
|
||||
@@ -981,7 +994,7 @@ function AppShell({
|
||||
maxLat: b.north,
|
||||
minLng: b.west,
|
||||
maxLng: b.east,
|
||||
includeDeleted: showDeleted,
|
||||
includeDeleted: showDeleted || onlyDeleted,
|
||||
});
|
||||
if (seq === fetchSeqRef.current) {
|
||||
setAssets(list);
|
||||
@@ -1003,13 +1016,14 @@ function AppShell({
|
||||
fetchBboxAssets(bounds).catch((e) => setStatus(`Fetch failed: ${e}`));
|
||||
}, BBOX_FETCH_DEBOUNCE_MS);
|
||||
void showDeleted;
|
||||
void onlyDeleted;
|
||||
return () => {
|
||||
if (debounceRef.current !== null) {
|
||||
window.clearTimeout(debounceRef.current);
|
||||
debounceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [bounds, showDeleted, osmEditMode, lassoMode, drawRoadMode]);
|
||||
}, [bounds, showDeleted, onlyDeleted, osmEditMode, lassoMode, drawRoadMode]);
|
||||
|
||||
async function handlePositionChange(
|
||||
id: number,
|
||||
@@ -1169,12 +1183,14 @@ function AppShell({
|
||||
setRenameInput(selectedAsset.asset_name ?? "");
|
||||
return;
|
||||
}
|
||||
// Delete / Backspace: when an asset is selected and we're not in any
|
||||
// edit mode that already claims those keys (OSM vertex/road delete,
|
||||
// link deletion handled above), delete the asset and toast.
|
||||
// Delete / Backspace: when an asset (or multi-selection) is active and
|
||||
// we're not in any edit mode that already claims those keys (OSM
|
||||
// vertex/road delete, link deletion handled above), delete and toast.
|
||||
// Multi-selection (Ctrl/Cmd+click) wins over single-select so a stray
|
||||
// single-selected asset isn't accidentally deleted alone.
|
||||
if (
|
||||
(e.key === "Delete" || e.key === "Backspace") &&
|
||||
selectedAsset &&
|
||||
(multiSelectedIds.size > 0 || selectedAsset) &&
|
||||
!osmEditMode &&
|
||||
!selectedLink
|
||||
) {
|
||||
@@ -1184,6 +1200,33 @@ function AppShell({
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
if (multiSelectedIds.size > 0) {
|
||||
const ids = Array.from(multiSelectedIds);
|
||||
(async () => {
|
||||
try {
|
||||
await invoke<number>("delete_assets_bulk", { ids });
|
||||
setMultiSelectedIds(new Set());
|
||||
const deletedSet = new Set(ids);
|
||||
setAssets((prev) =>
|
||||
prev.map((a) =>
|
||||
deletedSet.has(a.id) ? { ...a, deleted: 1 } : a,
|
||||
),
|
||||
);
|
||||
if (selectedAssetId !== null && deletedSet.has(selectedAssetId)) {
|
||||
setSelectedAssetId(null);
|
||||
}
|
||||
showToast(`Deleted ${ids.length}`);
|
||||
void Promise.all([
|
||||
refreshActions().catch(() => {}),
|
||||
refreshDataCounts().catch(() => {}),
|
||||
]);
|
||||
} catch (err) {
|
||||
showToast(`Delete failed: ${err}`);
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
if (!selectedAsset) return;
|
||||
const label = `${selectedAsset.asset_name} · ${selectedAsset.row_id}`;
|
||||
(async () => {
|
||||
try {
|
||||
@@ -1224,14 +1267,17 @@ function AppShell({
|
||||
setBboxMode((v) => !v);
|
||||
return;
|
||||
}
|
||||
// 'H' / 'h' toggles the asset image popup. Same input-guard rules as 'O'.
|
||||
// 'H' / 'h' toggles the asset image popup. Ctrl/Cmd+H is intentionally
|
||||
// allowed (and preventDefault'd) so the user can hide the popup while
|
||||
// multi-selecting — they're already holding Ctrl to extend the
|
||||
// selection. Alt+H stays browser/menu-bound (no preventDefault).
|
||||
if (e.key === "h" || e.key === "H") {
|
||||
const target = e.target as HTMLElement | null;
|
||||
const tag = target?.tagName;
|
||||
if (tag === "INPUT" || tag === "TEXTAREA" || target?.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (e.altKey) return;
|
||||
e.preventDefault();
|
||||
setPopupEnabled((v) => !v);
|
||||
return;
|
||||
@@ -1294,7 +1340,7 @@ function AppShell({
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [drawRoadMode, editingRoadId, linkPickMode, rightClickFirstId, lassoMode, selectedLink, selectedMetadataVideo, gotoPin, osmEditMode, markedVertices, focusedVideo, selectedRoadIds, lensActive, selectedAsset, popupEnabled]);
|
||||
}, [drawRoadMode, editingRoadId, linkPickMode, rightClickFirstId, lassoMode, selectedLink, selectedMetadataVideo, gotoPin, osmEditMode, markedVertices, focusedVideo, selectedRoadIds, lensActive, selectedAsset, selectedAssetId, popupEnabled, multiSelectedIds]);
|
||||
|
||||
// Image prefetch: when an asset is selected, warm the browser cache for its
|
||||
// immediate neighbours so [/] navigation feels instant.
|
||||
@@ -3506,11 +3552,24 @@ function AppShell({
|
||||
}}
|
||||
layerVisibility={layerVisibility}
|
||||
selectedAsset={selectedAsset}
|
||||
onSelect={(a) => {
|
||||
multiSelectedIds={Array.from(multiSelectedIds)}
|
||||
onSelect={(a, additive) => {
|
||||
if (additive) {
|
||||
if (!a) return;
|
||||
setMultiSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(a.id)) next.delete(a.id);
|
||||
else next.add(a.id);
|
||||
return next;
|
||||
});
|
||||
setStatus(`Multi-select: ${a.asset_name} · ${a.row_id}`);
|
||||
return;
|
||||
}
|
||||
if (linkPickMode && a && selectedAsset && a.id !== selectedAsset.id) {
|
||||
handleSetLink(a.id);
|
||||
return;
|
||||
}
|
||||
setMultiSelectedIds(new Set());
|
||||
setSelectedAssetId(a?.id ?? null);
|
||||
setSelectedLink(null);
|
||||
setStatus(a ? `Selected ${a.asset_name} · ${a.row_id}` : "");
|
||||
@@ -3678,7 +3737,19 @@ function AppShell({
|
||||
}}
|
||||
layerVisibility={layerVisibility}
|
||||
selectedAsset={selectedAsset}
|
||||
onSelect={(a) => {
|
||||
multiSelectedIds={Array.from(multiSelectedIds)}
|
||||
onSelect={(a, additive) => {
|
||||
if (additive) {
|
||||
if (!a) return;
|
||||
setMultiSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(a.id)) next.delete(a.id);
|
||||
else next.add(a.id);
|
||||
return next;
|
||||
});
|
||||
return;
|
||||
}
|
||||
setMultiSelectedIds(new Set());
|
||||
setSelectedAssetId(a?.id ?? null);
|
||||
setStatus(a ? `Selected ${a.asset_name} · ${a.row_id}` : "");
|
||||
}}
|
||||
@@ -3906,10 +3977,19 @@ function AppShell({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showDeleted}
|
||||
disabled={onlyDeleted}
|
||||
onChange={(e) => setShowDeleted(e.target.checked)}
|
||||
/>
|
||||
<span className="label">Show deleted</span>
|
||||
</label>
|
||||
<label className="filter-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={onlyDeleted}
|
||||
onChange={(e) => setOnlyDeleted(e.target.checked)}
|
||||
/>
|
||||
<span className="label">Only deleted</span>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user