Fix Ctrl+Click selection, add Delete key with confirmation dialog
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
eb42fbabfd
commit
f11190ee25
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.96",
|
"version": "1.4.97",
|
||||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -75,6 +75,7 @@ export function defaultSettings(): AppSettings {
|
|||||||
theme: "dark" as const,
|
theme: "dark" as const,
|
||||||
collapseNewPackages: true,
|
collapseNewPackages: true,
|
||||||
autoSkipExtracted: false,
|
autoSkipExtracted: false,
|
||||||
|
confirmDeleteSelection: true,
|
||||||
bandwidthSchedules: []
|
bandwidthSchedules: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -108,6 +108,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
minimizeToTray: Boolean(settings.minimizeToTray),
|
minimizeToTray: Boolean(settings.minimizeToTray),
|
||||||
collapseNewPackages: settings.collapseNewPackages !== undefined ? Boolean(settings.collapseNewPackages) : defaults.collapseNewPackages,
|
collapseNewPackages: settings.collapseNewPackages !== undefined ? Boolean(settings.collapseNewPackages) : defaults.collapseNewPackages,
|
||||||
autoSkipExtracted: settings.autoSkipExtracted !== undefined ? Boolean(settings.autoSkipExtracted) : defaults.autoSkipExtracted,
|
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,
|
theme: VALID_THEMES.has(settings.theme) ? settings.theme : defaults.theme,
|
||||||
bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules)
|
bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules)
|
||||||
};
|
};
|
||||||
|
|||||||
@ -473,6 +473,7 @@ export function App(): ReactElement {
|
|||||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||||
const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
|
const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<{ ids: Set<string>; dontAsk: boolean } | null>(null);
|
||||||
|
|
||||||
const currentCollectorTab = collectorTabs.find((t) => t.id === activeCollectorTab) ?? collectorTabs[0];
|
const currentCollectorTab = collectorTabs.find((t) => t.id === activeCollectorTab) ?? collectorTabs[0];
|
||||||
|
|
||||||
@ -1418,8 +1419,11 @@ export function App(): ReactElement {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const dragSelectRef = useRef(false);
|
const dragSelectRef = useRef(false);
|
||||||
|
const dragAnchorRef = useRef<string | null>(null);
|
||||||
|
const dragDidMoveRef = useRef(false);
|
||||||
|
|
||||||
const onSelectId = useCallback((id: string, ctrlKey: boolean): void => {
|
const onSelectId = useCallback((id: string, ctrlKey: boolean): void => {
|
||||||
|
if (dragDidMoveRef.current) return; // drag handled it, skip click
|
||||||
setSelectedIds((prev) => {
|
setSelectedIds((prev) => {
|
||||||
if (ctrlKey) {
|
if (ctrlKey) {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@ -1435,13 +1439,26 @@ export function App(): ReactElement {
|
|||||||
if (!e.ctrlKey || e.button !== 0) return;
|
if (!e.ctrlKey || e.button !== 0) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragSelectRef.current = true;
|
dragSelectRef.current = true;
|
||||||
setSelectedIds((prev) => { const next = new Set(prev); next.add(id); return next; });
|
dragAnchorRef.current = id;
|
||||||
const onUp = (): void => { dragSelectRef.current = false; window.removeEventListener("mouseup", onUp); };
|
dragDidMoveRef.current = false;
|
||||||
|
const onUp = (): void => {
|
||||||
|
dragSelectRef.current = false;
|
||||||
|
dragAnchorRef.current = null;
|
||||||
|
window.removeEventListener("mouseup", onUp);
|
||||||
|
};
|
||||||
window.addEventListener("mouseup", onUp);
|
window.addEventListener("mouseup", onUp);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSelectMouseEnter = useCallback((id: string): void => {
|
const onSelectMouseEnter = useCallback((id: string): void => {
|
||||||
if (!dragSelectRef.current) return;
|
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; });
|
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]);
|
}, [contextMenu]);
|
||||||
|
|
||||||
useEffect(() => {
|
const executeDeleteSelection = useCallback((ids: Set<string>): 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;
|
if (selectedIds.size === 0) return;
|
||||||
const onKey = (e: KeyboardEvent): void => { if (e.key === "Escape") setSelectedIds(new Set()); };
|
if (!settingsDraft.confirmDeleteSelection) {
|
||||||
const onClick = (e: MouseEvent): void => {
|
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;
|
const target = e.target as HTMLElement;
|
||||||
if (target.closest(".package-card")) return;
|
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") || target.closest(".ctx-menu")) return;
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
};
|
};
|
||||||
window.addEventListener("keydown", onKey);
|
window.addEventListener("keydown", onKey);
|
||||||
window.addEventListener("click", onClick);
|
window.addEventListener("mousedown", onDown);
|
||||||
return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("click", onClick); };
|
return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("mousedown", onDown); };
|
||||||
}, [selectedIds.size]);
|
}, [selectedIds, requestDeleteSelection]);
|
||||||
|
|
||||||
const onExportBackup = async (): Promise<void> => {
|
const onExportBackup = async (): Promise<void> => {
|
||||||
closeMenus();
|
closeMenus();
|
||||||
@ -2196,6 +2237,7 @@ export function App(): ReactElement {
|
|||||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.collapseNewPackages} onChange={(e) => setBool("collapseNewPackages", e.target.checked)} /> Neue Pakete eingeklappt</label>
|
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.collapseNewPackages} onChange={(e) => setBool("collapseNewPackages", e.target.checked)} /> Neue Pakete eingeklappt</label>
|
||||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.clipboardWatch} onChange={(e) => setBool("clipboardWatch", e.target.checked)} /> Zwischenablage überwachen</label>
|
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.clipboardWatch} onChange={(e) => setBool("clipboardWatch", e.target.checked)} /> Zwischenablage überwachen</label>
|
||||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.minimizeToTray} onChange={(e) => setBool("minimizeToTray", e.target.checked)} /> In System Tray minimieren</label>
|
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.minimizeToTray} onChange={(e) => setBool("minimizeToTray", e.target.checked)} /> In System Tray minimieren</label>
|
||||||
|
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.confirmDeleteSelection} onChange={(e) => setBool("confirmDeleteSelection", e.target.checked)} /> Vor dem Löschen bestätigen</label>
|
||||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.theme === "light"} onChange={(e) => {
|
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.theme === "light"} onChange={(e) => {
|
||||||
const next = e.target.checked ? "light" : "dark";
|
const next = e.target.checked ? "light" : "dark";
|
||||||
settingsDraftRevisionRef.current += 1;
|
settingsDraftRevisionRef.current += 1;
|
||||||
@ -2362,6 +2404,38 @@ export function App(): ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{deleteConfirm && (() => {
|
||||||
|
const itemCount = [...deleteConfirm.ids].filter((id) => snapshot.session.items[id]).length;
|
||||||
|
const pkgCount = [...deleteConfirm.ids].filter((id) => snapshot.session.packages[id]).length;
|
||||||
|
const totalRemaining = Object.keys(snapshot.session.items).length + Object.keys(snapshot.session.packages).length - itemCount - pkgCount;
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (pkgCount > 0) parts.push(`${pkgCount} Paket(e)`);
|
||||||
|
if (itemCount > 0) parts.push(`${itemCount} Link(s)`);
|
||||||
|
return (
|
||||||
|
<div className="modal-backdrop" onClick={() => setDeleteConfirm(null)}>
|
||||||
|
<div className="modal-card" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h3>Bist Du Dir sicher?</h3>
|
||||||
|
<p>Möchtest Du wirklich diese Aufräumaktion(en) durchführen?<br />Ausgewählte Links löschen</p>
|
||||||
|
<p><strong>Zu erledigende Aufgaben:</strong><br />{parts.join(" + ")} löschen – {totalRemaining} Link(s) verbleiben!</p>
|
||||||
|
<label className="toggle-line">
|
||||||
|
<input type="checkbox" checked={deleteConfirm.dontAsk} onChange={(e) => setDeleteConfirm((prev) => prev ? { ...prev, dontAsk: e.target.checked } : prev)} />
|
||||||
|
Nicht mehr anzeigen
|
||||||
|
</label>
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button className="btn" onClick={() => setDeleteConfirm(null)}>Abbrechen</button>
|
||||||
|
<button className="btn danger" onClick={() => {
|
||||||
|
if (deleteConfirm.dontAsk) {
|
||||||
|
setBool("confirmDeleteSelection", false);
|
||||||
|
}
|
||||||
|
executeDeleteSelection(deleteConfirm.ids);
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
}}>Fortfahren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{startConflictPrompt && (
|
{startConflictPrompt && (
|
||||||
<div className="modal-backdrop" onClick={() => closeStartConflictPrompt(null)}>
|
<div className="modal-backdrop" onClick={() => closeStartConflictPrompt(null)}>
|
||||||
<div className="modal-card" onClick={(event) => event.stopPropagation()}>
|
<div className="modal-card" onClick={(event) => event.stopPropagation()}>
|
||||||
@ -2570,7 +2644,7 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
|
|||||||
className={`package-card${pkg.enabled ? "" : " disabled-pkg"}${selectedIds.has(pkg.id) ? " pkg-selected" : ""}`}
|
className={`package-card${pkg.enabled ? "" : " disabled-pkg"}${selectedIds.has(pkg.id) ? " pkg-selected" : ""}`}
|
||||||
draggable
|
draggable
|
||||||
onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(pkg.id, undefined, e.clientX, e.clientY); }}
|
onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(pkg.id, undefined, e.clientX, e.clientY); }}
|
||||||
onClick={(e) => { onSelect(pkg.id, e.ctrlKey); }}
|
onClick={(e) => { if (e.ctrlKey) onSelect(pkg.id, true); }}
|
||||||
onMouseDown={(e) => onSelectMouseDown(pkg.id, e)}
|
onMouseDown={(e) => onSelectMouseDown(pkg.id, e)}
|
||||||
onMouseEnter={() => onSelectMouseEnter(pkg.id)}
|
onMouseEnter={() => onSelectMouseEnter(pkg.id)}
|
||||||
onDragStart={(event) => { event.stopPropagation(); onDragStart(pkg.id); }}
|
onDragStart={(event) => { event.stopPropagation(); onDragStart(pkg.id); }}
|
||||||
|
|||||||
@ -75,6 +75,7 @@ export interface AppSettings {
|
|||||||
theme: AppTheme;
|
theme: AppTheme;
|
||||||
collapseNewPackages: boolean;
|
collapseNewPackages: boolean;
|
||||||
autoSkipExtracted: boolean;
|
autoSkipExtracted: boolean;
|
||||||
|
confirmDeleteSelection: boolean;
|
||||||
bandwidthSchedules: BandwidthScheduleEntry[];
|
bandwidthSchedules: BandwidthScheduleEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user