From 545043e1d689fc8a20b371ff003cab1b7550858e Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 4 Mar 2026 00:06:58 +0100 Subject: [PATCH] Add multi-select, Ctrl+A, right-click context menu with link viewer to history tab Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- src/main/download-manager.ts | 3 +- src/renderer/App.tsx | 110 +++++++++++++++++++++++++++++++++-- src/shared/types.ts | 1 + 4 files changed, 109 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 09de4f8..645c475 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.5.81", + "version": "1.5.82", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 7e32388..5862137 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -3297,7 +3297,8 @@ export class DownloadManager extends EventEmitter { completedAt: nowMs(), durationSeconds, status: reason === "completed" ? "completed" : "deleted", - outputDir: pkg.outputDir + outputDir: pkg.outputDir, + urls: completedItems.map(item => item.url).filter(Boolean), }; this.onHistoryEntryCallback(entry); } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index ae9ddbe..0b49664 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -512,7 +512,11 @@ export function App(): ReactElement { const [selectedIds, setSelectedIds] = useState>(new Set()); const [deleteConfirm, setDeleteConfirm] = useState<{ ids: Set; dontAsk: boolean } | null>(null); const [historyEntries, setHistoryEntries] = useState([]); + const historyEntriesRef = useRef([]); const [historyCollapsed, setHistoryCollapsed] = useState>({}); + const [selectedHistoryIds, setSelectedHistoryIds] = useState>(new Set()); + const [historyCtxMenu, setHistoryCtxMenu] = useState<{ x: number; y: number; entryId: string } | null>(null); + const historyCtxMenuRef = useRef(null); // Load history when tab changes to history useEffect(() => { @@ -531,6 +535,8 @@ export function App(): ReactElement { void loadHistory(); }, [tab]); + useEffect(() => { historyEntriesRef.current = historyEntries; }, [historyEntries]); + const currentCollectorTab = collectorTabs.find((t) => t.id === activeCollectorTab) ?? collectorTabs[0]; useEffect(() => { @@ -1664,6 +1670,29 @@ export function App(): ReactElement { } }, [contextMenu]); + useEffect(() => { + if (!historyCtxMenu) return; + const close = (): void => setHistoryCtxMenu(null); + window.addEventListener("click", close); + window.addEventListener("contextmenu", close); + return () => { + window.removeEventListener("click", close); + window.removeEventListener("contextmenu", close); + }; + }, [historyCtxMenu]); + + useLayoutEffect(() => { + if (!historyCtxMenu || !historyCtxMenuRef.current) return; + const el = historyCtxMenuRef.current; + const rect = el.getBoundingClientRect(); + if (rect.bottom > window.innerHeight) { + el.style.top = `${Math.max(0, historyCtxMenu.y - rect.height)}px`; + } + if (rect.right > window.innerWidth) { + el.style.left = `${Math.max(0, historyCtxMenu.x - rect.width)}px`; + } + }, [historyCtxMenu]); + const executeDeleteSelection = useCallback((ids: Set): void => { const current = snapshotRef.current; for (const id of ids) { @@ -1773,6 +1802,9 @@ export function App(): ReactElement { if (tabRef.current === "downloads") { e.preventDefault(); setSelectedIds(new Set(Object.keys(snapshotRef.current.session.packages))); + } else if (tabRef.current === "history") { + e.preventDefault(); + setSelectedHistoryIds(new Set(historyEntriesRef.current.map(e => e.id))); } return; } @@ -2244,17 +2276,50 @@ export function App(): ReactElement { {tab === "history" && (
- {historyEntries.length} Paket{historyEntries.length !== 1 ? "e" : ""} im Verlauf + + {selectedHistoryIds.size > 0 + ? `${selectedHistoryIds.size} von ${historyEntries.length} ausgewählt` + : `${historyEntries.length} Paket${historyEntries.length !== 1 ? "e" : ""} im Verlauf`} + + {selectedHistoryIds.size > 0 && ( + + )} {historyEntries.length > 0 && ( - + )}
{historyEntries.length === 0 &&
Noch keine abgeschlossenen Pakete im Verlauf.
} {historyEntries.map((entry) => { const collapsed = historyCollapsed[entry.id] ?? true; + const isSelected = selectedHistoryIds.has(entry.id); return ( -
-
setHistoryCollapsed((prev) => ({ ...prev, [entry.id]: !collapsed }))} style={{ cursor: "pointer" }}> +
{ + if (e.ctrlKey) { + e.preventDefault(); + setSelectedHistoryIds((prev) => { + const next = new Set(prev); + if (next.has(entry.id)) next.delete(entry.id); else next.add(entry.id); + return next; + }); + } + }} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + setSelectedHistoryIds((prev) => prev.has(entry.id) ? prev : new Set([entry.id])); + setHistoryCtxMenu({ x: e.clientX, y: e.clientY, entryId: entry.id }); + }} + > +
{ if (e.ctrlKey) return; setHistoryCollapsed((prev) => ({ ...prev, [entry.id]: !collapsed })); }} style={{ cursor: "pointer" }}>
@@ -2302,7 +2367,7 @@ export function App(): ReactElement { {entry.status === "completed" ? "Abgeschlossen" : "Gelöscht"}
- +
)} @@ -2760,6 +2825,41 @@ export function App(): ReactElement { ); })()} + {historyCtxMenu && (() => { + const multi = selectedHistoryIds.size > 1; + const contextEntry = historyEntries.find(e => e.id === historyCtxMenu.entryId); + const hasUrls = (contextEntry?.urls?.length ?? 0) > 0; + const removeSelected = (): void => { + const ids = [...selectedHistoryIds]; + void Promise.all(ids.map(id => window.rd.removeHistoryEntry(id))).then(() => { + setHistoryEntries((prev) => prev.filter((e) => !selectedHistoryIds.has(e.id))); + setSelectedHistoryIds(new Set()); + }); + setHistoryCtxMenu(null); + }; + return ( +
e.stopPropagation()}> + + {hasUrls && !multi && ( + <> +
+ + + )} +
+ +
+ ); + })()} {linkPopup && (
setLinkPopup(null)}>
e.stopPropagation()}> diff --git a/src/shared/types.ts b/src/shared/types.ts index 4c2881c..c8cd1ce 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -268,6 +268,7 @@ export interface HistoryEntry { durationSeconds: number; status: "completed" | "deleted"; outputDir: string; + urls?: string[]; } export interface HistoryState {