multi slct del

This commit is contained in:
2026-05-26 15:34:06 +05:30
parent b99d02e29a
commit f83724951f
4 changed files with 269 additions and 43 deletions

View File

@@ -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>
</>
)}