From f11190ee2542b25e12418d449f0680317e4f2bfe Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Mon, 2 Mar 2026 18:31:45 +0100 Subject: [PATCH] Fix Ctrl+Click selection, add Delete key with confirmation dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Ctrl+Click: mousedown no longer immediately adds item, preventing onClick from toggling it back off. Drag-select still works via mouseenter. - Delete key removes selected items/packages with JDownloader-style confirmation dialog showing count and remaining items. - "Nicht mehr anzeigen" checkbox disables future confirmations. - New setting "Vor dem Löschen bestätigen" under Allgemein. Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/main/constants.ts | 1 + src/main/storage.ts | 1 + src/renderer/App.tsx | 94 ++++++++++++++++++++++++++++++++++++++----- src/shared/types.ts | 1 + 5 files changed, 88 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index e29f4ed..26749be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.4.96", + "version": "1.4.97", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/constants.ts b/src/main/constants.ts index af111e9..51c69db 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -75,6 +75,7 @@ export function defaultSettings(): AppSettings { theme: "dark" as const, collapseNewPackages: true, autoSkipExtracted: false, + confirmDeleteSelection: true, bandwidthSchedules: [] }; } diff --git a/src/main/storage.ts b/src/main/storage.ts index 1b8e390..36095b5 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -108,6 +108,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings { minimizeToTray: Boolean(settings.minimizeToTray), collapseNewPackages: settings.collapseNewPackages !== undefined ? Boolean(settings.collapseNewPackages) : defaults.collapseNewPackages, autoSkipExtracted: settings.autoSkipExtracted !== undefined ? Boolean(settings.autoSkipExtracted) : defaults.autoSkipExtracted, + confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection, theme: VALID_THEMES.has(settings.theme) ? settings.theme : defaults.theme, bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules) }; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index e1e48db..14b1dbe 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -473,6 +473,7 @@ export function App(): ReactElement { const [contextMenu, setContextMenu] = useState(null); const [linkPopup, setLinkPopup] = useState(null); const [selectedIds, setSelectedIds] = useState>(new Set()); + const [deleteConfirm, setDeleteConfirm] = useState<{ ids: Set; dontAsk: boolean } | null>(null); const currentCollectorTab = collectorTabs.find((t) => t.id === activeCollectorTab) ?? collectorTabs[0]; @@ -1418,8 +1419,11 @@ export function App(): ReactElement { }, []); const dragSelectRef = useRef(false); + const dragAnchorRef = useRef(null); + const dragDidMoveRef = useRef(false); const onSelectId = useCallback((id: string, ctrlKey: boolean): void => { + if (dragDidMoveRef.current) return; // drag handled it, skip click setSelectedIds((prev) => { if (ctrlKey) { const next = new Set(prev); @@ -1435,13 +1439,26 @@ export function App(): ReactElement { if (!e.ctrlKey || e.button !== 0) return; e.preventDefault(); dragSelectRef.current = true; - setSelectedIds((prev) => { const next = new Set(prev); next.add(id); return next; }); - const onUp = (): void => { dragSelectRef.current = false; window.removeEventListener("mouseup", onUp); }; + dragAnchorRef.current = id; + dragDidMoveRef.current = false; + const onUp = (): void => { + dragSelectRef.current = false; + dragAnchorRef.current = null; + window.removeEventListener("mouseup", onUp); + }; window.addEventListener("mouseup", onUp); }, []); const onSelectMouseEnter = useCallback((id: string): void => { if (!dragSelectRef.current) return; + if (!dragDidMoveRef.current) { + dragDidMoveRef.current = true; + // Add anchor item now that we know it's a drag + const anchor = dragAnchorRef.current; + if (anchor) { + setSelectedIds((prev) => { if (prev.has(anchor)) return prev; const next = new Set(prev); next.add(anchor); return next; }); + } + } setSelectedIds((prev) => { if (prev.has(id)) return prev; const next = new Set(prev); next.add(id); return next; }); }, []); @@ -1559,18 +1576,42 @@ export function App(): ReactElement { }; }, [contextMenu]); - useEffect(() => { + const executeDeleteSelection = useCallback((ids: Set): void => { + for (const id of ids) { + if (snapshot.session.items[id]) void window.rd.removeItem(id); + else if (snapshot.session.packages[id]) onPackageCancel(id); + } + setSelectedIds(new Set()); + }, [snapshot.session.items, snapshot.session.packages, onPackageCancel]); + + const requestDeleteSelection = useCallback((): void => { if (selectedIds.size === 0) return; - const onKey = (e: KeyboardEvent): void => { if (e.key === "Escape") setSelectedIds(new Set()); }; - const onClick = (e: MouseEvent): void => { + if (!settingsDraft.confirmDeleteSelection) { + executeDeleteSelection(selectedIds); + return; + } + setDeleteConfirm({ ids: new Set(selectedIds), dontAsk: false }); + }, [selectedIds, settingsDraft.confirmDeleteSelection, executeDeleteSelection]); + + useEffect(() => { + const onKey = (e: KeyboardEvent): void => { + if (e.key === "Escape") setSelectedIds(new Set()); + if (e.key === "Delete" && selectedIds.size > 0) { + const target = e.target as HTMLElement; + if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") return; + e.preventDefault(); + requestDeleteSelection(); + } + }; + const onDown = (e: MouseEvent): void => { const target = e.target as HTMLElement; - if (target.closest(".package-card")) return; + if (target.closest(".package-card") || target.closest(".ctx-menu")) return; setSelectedIds(new Set()); }; window.addEventListener("keydown", onKey); - window.addEventListener("click", onClick); - return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("click", onClick); }; - }, [selectedIds.size]); + window.addEventListener("mousedown", onDown); + return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("mousedown", onDown); }; + }, [selectedIds, requestDeleteSelection]); const onExportBackup = async (): Promise => { closeMenus(); @@ -2196,6 +2237,7 @@ export function App(): ReactElement { +