Release v1.6.23

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 19:26:01 +01:00
parent 9cceaacd14
commit 26b2ef0abb
8 changed files with 107 additions and 36 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.6.22", "version": "1.6.23",
"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",

View File

@ -21,7 +21,7 @@ import { parseCollectorInput } from "./link-parser";
import { configureLogger, getLogFilePath, logger } from "./logger"; import { configureLogger, getLogFilePath, logger } from "./logger";
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log"; import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
import { MegaWebFallback } from "./mega-web-fallback"; import { MegaWebFallback } from "./mega-web-fallback";
import { addHistoryEntry, clearHistory, createStoragePaths, loadHistory, loadSession, loadSettings, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, saveSession, saveSettings } from "./storage"; import { addHistoryEntry, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistory, loadSession, loadSettings, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, saveSession, saveSettings } from "./storage";
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update"; import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
import { startDebugServer, stopDebugServer } from "./debug-server"; import { startDebugServer, stopDebugServer } from "./debug-server";
@ -306,9 +306,10 @@ export class AppController {
// so no extraction tasks from it should keep running. // so no extraction tasks from it should keep running.
this.manager.stop(); this.manager.stop();
this.manager.abortAllPostProcessing(); this.manager.abortAllPostProcessing();
// Cancel any deferred persist timer so the old in-memory session // Cancel any deferred persist timer and queued async writes so the old
// does not overwrite the restored session file on disk. // in-memory session does not overwrite the restored session file on disk.
this.manager.clearPersistTimer(); this.manager.clearPersistTimer();
cancelPendingAsyncSaves();
const restoredSession = normalizeLoadedSessionTransientFields( const restoredSession = normalizeLoadedSessionTransientFields(
normalizeLoadedSession(parsed.session) normalizeLoadedSession(parsed.session)
); );

View File

@ -669,7 +669,11 @@ class BestDebridClient {
try { try {
return await this.tryRequest(request, link, signal); return await this.tryRequest(request, link, signal);
} catch (error) { } catch (error) {
lastError = compactErrorText(error); const errorText = compactErrorText(error);
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
throw error;
}
lastError = errorText;
} }
} }

View File

@ -32,7 +32,7 @@ type ActiveTask = {
itemId: string; itemId: string;
packageId: string; packageId: string;
abortController: AbortController; abortController: AbortController;
abortReason: "stop" | "cancel" | "reconnect" | "package_toggle" | "stall" | "shutdown" | "none"; abortReason: "stop" | "cancel" | "reconnect" | "package_toggle" | "stall" | "shutdown" | "reset" | "none";
resumable: boolean; resumable: boolean;
nonResumableCounted: boolean; nonResumableCounted: boolean;
freshRetryUsed?: boolean; freshRetryUsed?: boolean;
@ -1509,10 +1509,14 @@ export class DownloadManager extends EventEmitter {
} }
} }
this.runCompletedPackages.delete(packageId); this.runCompletedPackages.delete(packageId);
if (this.session.running) {
this.runPackageIds.add(packageId);
}
pkg.status = "queued"; pkg.status = "queued";
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
this.persistSoon(); this.persistSoon();
this.emitState(true); this.emitState(true);
void this.ensureScheduler().catch((err) => logger.warn(`ensureScheduler Fehler (resolveStartConflict): ${compactErrorText(err)}`));
return { skipped: false, overwritten: true }; return { skipped: false, overwritten: true };
} }
@ -2557,6 +2561,9 @@ export class DownloadManager extends EventEmitter {
logger.info(`Paket "${pkg.name}" zurückgesetzt (${itemIds.length} Items)`); logger.info(`Paket "${pkg.name}" zurückgesetzt (${itemIds.length} Items)`);
this.persistSoon(); this.persistSoon();
this.emitState(true); this.emitState(true);
if (this.session.running) {
void this.ensureScheduler().catch((err) => logger.warn(`ensureScheduler Fehler (resetPackage): ${compactErrorText(err)}`));
}
} }
public resetItems(itemIds: string[]): void { public resetItems(itemIds: string[]): void {
@ -2615,11 +2622,18 @@ export class DownloadManager extends EventEmitter {
pkg.cancelled = false; pkg.cancelled = false;
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
} }
// Re-add package to runPackageIds so scheduler picks up the reset items
if (this.session.running) {
this.runPackageIds.add(pkgId);
}
} }
logger.info(`${itemIds.length} Item(s) zurückgesetzt`); logger.info(`${itemIds.length} Item(s) zurückgesetzt`);
this.persistSoon(); this.persistSoon();
this.emitState(true); this.emitState(true);
if (this.session.running) {
void this.ensureScheduler().catch((err) => logger.warn(`ensureScheduler Fehler (resetItems): ${compactErrorText(err)}`));
}
} }
public setPackagePriority(packageId: string, priority: PackagePriority): void { public setPackagePriority(packageId: string, priority: PackagePriority): void {
@ -2660,6 +2674,8 @@ export class DownloadManager extends EventEmitter {
item.fullStatus = "Übersprungen"; item.fullStatus = "Übersprungen";
item.speedBps = 0; item.speedBps = 0;
item.updatedAt = nowMs(); item.updatedAt = nowMs();
this.retryAfterByItem.delete(itemId);
this.retryStateByItem.delete(itemId);
this.recordRunOutcome(itemId, "cancelled"); this.recordRunOutcome(itemId, "cancelled");
} }
this.persistSoon(); this.persistSoon();
@ -3099,8 +3115,9 @@ export class DownloadManager extends EventEmitter {
this.retryAfterByItem.clear(); this.retryAfterByItem.clear();
this.nonResumableActive = 0; this.nonResumableActive = 0;
this.session.summaryText = ""; this.session.summaryText = "";
this.lastSettingsPersistAt = 0; // force settings save on shutdown // Persist synchronously on shutdown to guarantee data is written before process exits
this.persistNow(); saveSession(this.storagePaths, this.session);
saveSettings(this.storagePaths, this.settings);
this.emitState(true); this.emitState(true);
logger.info(`Shutdown-Vorbereitung beendet: requeued=${requeuedItems}`); logger.info(`Shutdown-Vorbereitung beendet: requeued=${requeuedItems}`);
} }
@ -3272,6 +3289,10 @@ export class DownloadManager extends EventEmitter {
return false; return false;
} }
if (item.status === "completed") { if (item.status === "completed") {
// With autoExtract: keep items that haven't been extracted yet
if (this.settings.autoExtract && !isExtractedLabel(item.fullStatus || "")) {
return true;
}
delete this.session.items[itemId]; delete this.session.items[itemId];
this.itemCount = Math.max(0, this.itemCount - 1); this.itemCount = Math.max(0, this.itemCount - 1);
return false; return false;
@ -3279,8 +3300,7 @@ export class DownloadManager extends EventEmitter {
return true; return true;
}); });
if (pkg.itemIds.length === 0) { if (pkg.itemIds.length === 0) {
delete this.session.packages[pkgId]; this.removePackageFromSession(pkgId, []);
this.session.packageOrder = this.session.packageOrder.filter((id) => id !== pkgId);
} }
} }
} }
@ -3744,7 +3764,7 @@ export class DownloadManager extends EventEmitter {
const targetStatus = failed > 0 const targetStatus = failed > 0
? "failed" ? "failed"
: cancelled > 0 : cancelled > 0
? (success > 0 ? "failed" : "cancelled") ? (success > 0 ? "completed" : "cancelled")
: "completed"; : "completed";
if (pkg.status !== targetStatus) { if (pkg.status !== targetStatus) {
pkg.status = targetStatus; pkg.status = targetStatus;
@ -3897,8 +3917,9 @@ export class DownloadManager extends EventEmitter {
const pkg = this.session.packages[packageId]; const pkg = this.session.packages[packageId];
// Only create history here for deletions — completions are handled by recordPackageHistory // Only create history here for deletions — completions are handled by recordPackageHistory
if (pkg && this.onHistoryEntryCallback && reason === "deleted" && !this.historyRecordedPackages.has(packageId)) { if (pkg && this.onHistoryEntryCallback && reason === "deleted" && !this.historyRecordedPackages.has(packageId)) {
const completedItems = itemIds.map(id => this.session.items[id]).filter(Boolean) as DownloadItem[]; const allItems = itemIds.map(id => this.session.items[id]).filter(Boolean) as DownloadItem[];
const completedCount = completedItems.filter(item => item.status === "completed").length; const completedItems = allItems.filter(item => item.status === "completed");
const completedCount = completedItems.length;
if (completedCount > 0) { if (completedCount > 0) {
const totalBytes = completedItems.reduce((sum, item) => sum + (item.downloadedBytes || 0), 0); const totalBytes = completedItems.reduce((sum, item) => sum + (item.downloadedBytes || 0), 0);
const durationSeconds = pkg.createdAt > 0 ? Math.max(1, Math.floor((nowMs() - pkg.createdAt) / 1000)) : 1; const durationSeconds = pkg.createdAt > 0 ? Math.max(1, Math.floor((nowMs() - pkg.createdAt) / 1000)) : 1;
@ -5118,6 +5139,7 @@ export class DownloadManager extends EventEmitter {
const previouslyContributed = this.itemContributedBytes.get(active.itemId) || 0; const previouslyContributed = this.itemContributedBytes.get(active.itemId) || 0;
if (previouslyContributed > 0) { if (previouslyContributed > 0) {
this.session.totalDownloadedBytes = Math.max(0, this.session.totalDownloadedBytes - previouslyContributed); this.session.totalDownloadedBytes = Math.max(0, this.session.totalDownloadedBytes - previouslyContributed);
this.sessionDownloadedBytes = Math.max(0, this.sessionDownloadedBytes - previouslyContributed);
this.itemContributedBytes.set(active.itemId, 0); this.itemContributedBytes.set(active.itemId, 0);
} }
if (existingBytes > 0) { if (existingBytes > 0) {
@ -6673,11 +6695,13 @@ export class DownloadManager extends EventEmitter {
return; return;
} }
// With autoExtract: only remove once ALL items are extracted, not just downloaded // With autoExtract: only remove once all completed items are extracted (failed/cancelled don't need extraction)
if (this.settings.autoExtract) { if (this.settings.autoExtract) {
const allExtracted = pkg.itemIds.every((itemId) => { const allExtracted = pkg.itemIds.every((itemId) => {
const item = this.session.items[itemId]; const item = this.session.items[itemId];
return !item || isExtractedLabel(item.fullStatus || ""); if (!item) return true;
if (item.status === "failed" || item.status === "cancelled") return true;
return isExtractedLabel(item.fullStatus || "");
}); });
if (!allExtracted) { if (!allExtracted) {
return; return;
@ -6727,11 +6751,13 @@ export class DownloadManager extends EventEmitter {
return item != null && item.status !== "completed" && item.status !== "cancelled" && item.status !== "failed"; return item != null && item.status !== "completed" && item.status !== "cancelled" && item.status !== "failed";
}); });
if (!hasOpen) { if (!hasOpen) {
// With autoExtract: only remove once ALL items are extracted, not just downloaded // With autoExtract: only remove once completed items are extracted (failed/cancelled don't need extraction)
if (this.settings.autoExtract) { if (this.settings.autoExtract) {
const allExtracted = pkg.itemIds.every((id) => { const allExtracted = pkg.itemIds.every((id) => {
const item = this.session.items[id]; const item = this.session.items[id];
return !item || isExtractedLabel(item.fullStatus || ""); if (!item) return true;
if (item.status === "failed" || item.status === "cancelled") return true;
return isExtractedLabel(item.fullStatus || "");
}); });
if (!allExtracted) { if (!allExtracted) {
return; return;

View File

@ -2091,6 +2091,14 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
if (options.signal?.aborted || noExtractorEncountered) break; if (options.signal?.aborted || noExtractorEncountered) break;
await extractSingleArchive(archivePath); await extractSingleArchive(archivePath);
} }
// Count remaining archives as failed when no extractor was found
if (noExtractorEncountered) {
const remaining = candidates.length - (extracted + failed);
if (remaining > 0) {
failed += remaining;
emitProgress(candidates.length, "", "extracting", 0, 0);
}
}
} else { } else {
// Password discovery: extract first archive serially to find the correct password, // Password discovery: extract first archive serially to find the correct password,
// then run remaining archives in parallel with the promoted password order. // then run remaining archives in parallel with the promoted password order.

View File

@ -62,7 +62,8 @@ function isRetryableErrorText(text: string): boolean {
|| lower.includes("aborted") || lower.includes("aborted")
|| lower.includes("econnreset") || lower.includes("econnreset")
|| lower.includes("enotfound") || lower.includes("enotfound")
|| lower.includes("etimedout"); || lower.includes("etimedout")
|| lower.includes("html statt json");
} }
function withTimeoutSignal(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal { function withTimeoutSignal(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal {
@ -165,6 +166,15 @@ export class RealDebridClient {
if (!directUrl) { if (!directUrl) {
throw new Error("Unrestrict ohne Download-URL"); throw new Error("Unrestrict ohne Download-URL");
} }
try {
const parsedUrl = new URL(directUrl);
if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") {
throw new Error(`Ungültiges Download-URL-Protokoll (${parsedUrl.protocol})`);
}
} catch (urlError) {
if (urlError instanceof Error && urlError.message.includes("Protokoll")) throw urlError;
throw new Error("Real-Debrid Antwort enthält keine gültige Download-URL");
}
const fileName = String(payload.filename || "download.bin").trim() || "download.bin"; const fileName = String(payload.filename || "download.bin").trim() || "download.bin";
const fileSizeRaw = Number(payload.filesize ?? NaN); const fileSizeRaw = Number(payload.filesize ?? NaN);

View File

@ -339,7 +339,8 @@ export function normalizeLoadedSession(raw: unknown): SessionState {
return true; return true;
}); });
for (const packageId of Object.keys(packagesById)) { for (const packageId of Object.keys(packagesById)) {
if (!packageOrder.includes(packageId)) { if (!seenOrder.has(packageId)) {
seenOrder.add(packageId);
packageOrder.push(packageId); packageOrder.push(packageId);
} }
} }
@ -606,6 +607,11 @@ async function saveSessionPayloadAsync(paths: StoragePaths, payload: string): Pr
} }
} }
export function cancelPendingAsyncSaves(): void {
asyncSaveQueued = null;
asyncSettingsSaveQueued = null;
}
export async function saveSessionAsync(paths: StoragePaths, session: SessionState): Promise<void> { export async function saveSessionAsync(paths: StoragePaths, session: SessionState): Promise<void> {
const payload = JSON.stringify({ ...session, updatedAt: Date.now() }); const payload = JSON.stringify({ ...session, updatedAt: Date.now() });
await saveSessionPayloadAsync(paths, payload); await saveSessionPayloadAsync(paths, payload);

View File

@ -1169,7 +1169,7 @@ export function App(): ReactElement {
if (result.addedLinks > 0) { if (result.addedLinks > 0) {
showToast(`${result.addedPackages} Paket(e), ${result.addedLinks} Link(s) hinzugefügt`); showToast(`${result.addedPackages} Paket(e), ${result.addedLinks} Link(s) hinzugefügt`);
setCollectorTabs((prev) => prev.map((t) => t.id === activeId ? { ...t, text: "" } : t)); setCollectorTabs((prev) => prev.map((t) => t.id === activeId ? { ...t, text: "" } : t));
if (snapshot.settings.collapseNewPackages) { await collapseNewPackages(existingIds); } if (snapshotRef.current.settings.collapseNewPackages) { await collapseNewPackages(existingIds); }
} else { } else {
showToast("Keine gültigen Links gefunden"); showToast("Keine gültigen Links gefunden");
} }
@ -1187,7 +1187,7 @@ export function App(): ReactElement {
const result = await window.rd.addContainers(files); const result = await window.rd.addContainers(files);
if (result.addedLinks > 0) { if (result.addedLinks > 0) {
showToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`); showToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
if (snapshot.settings.collapseNewPackages) { await collapseNewPackages(existingIds); } if (snapshotRef.current.settings.collapseNewPackages) { await collapseNewPackages(existingIds); }
} else { } else {
showToast("Keine gültigen Links in den DLC-Dateien gefunden", 3000); showToast("Keine gültigen Links in den DLC-Dateien gefunden", 3000);
} }
@ -1214,7 +1214,7 @@ export function App(): ReactElement {
const result = await window.rd.addContainers(dlc); const result = await window.rd.addContainers(dlc);
if (result.addedLinks > 0) { if (result.addedLinks > 0) {
showToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`); showToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
if (snapshot.settings.collapseNewPackages) { await collapseNewPackages(existingIds); } if (snapshotRef.current.settings.collapseNewPackages) { await collapseNewPackages(existingIds); }
} else { } else {
showToast("Keine gültigen Links in den DLC-Dateien gefunden", 3000); showToast("Keine gültigen Links in den DLC-Dateien gefunden", 3000);
} }
@ -1434,13 +1434,16 @@ export function App(): ReactElement {
}, []); }, []);
const onPackageFinishEdit = useCallback((packageId: string, currentName: string, nextName: string): void => { const onPackageFinishEdit = useCallback((packageId: string, currentName: string, nextName: string): void => {
setEditingPackageId(null); setEditingPackageId((prev) => {
const normalized = nextName.trim(); if (prev !== packageId) return prev; // already finished (e.g. blur after Enter key)
if (normalized && normalized !== currentName.trim()) { const normalized = nextName.trim();
void window.rd.renamePackage(packageId, normalized).catch((error) => { if (normalized && normalized !== currentName.trim()) {
showToast(`Umbenennen fehlgeschlagen: ${String(error)}`, 2400); void window.rd.renamePackage(packageId, normalized).catch((error) => {
}); showToast(`Umbenennen fehlgeschlagen: ${String(error)}`, 2400);
} });
}
return null;
});
}, [showToast]); }, [showToast]);
const onPackageToggleCollapse = useCallback((packageId: string): void => { const onPackageToggleCollapse = useCallback((packageId: string): void => {
@ -1599,6 +1602,7 @@ export function App(): ReactElement {
const onUp = (): void => { const onUp = (): void => {
dragSelectRef.current = false; dragSelectRef.current = false;
dragAnchorRef.current = null; dragAnchorRef.current = null;
dragDidMoveRef.current = false;
window.removeEventListener("mouseup", onUp); window.removeEventListener("mouseup", onUp);
}; };
window.addEventListener("mouseup", onUp); window.addEventListener("mouseup", onUp);
@ -1619,18 +1623,20 @@ export function App(): ReactElement {
const showLinksPopup = useCallback((packageId: string, itemId?: string): void => { const showLinksPopup = useCallback((packageId: string, itemId?: string): void => {
const sel = selectedIds; const sel = selectedIds;
const currentPackages = snapshotRef.current.session.packages;
const currentItems = snapshotRef.current.session.items;
// Multi-select: collect links from all selected packages/items // Multi-select: collect links from all selected packages/items
if (sel.size > 1) { if (sel.size > 1) {
const allLinks: { name: string; url: string }[] = []; const allLinks: { name: string; url: string }[] = [];
for (const id of sel) { for (const id of sel) {
const pkg = snapshot.session.packages[id]; const pkg = currentPackages[id];
if (pkg) { if (pkg) {
for (const iid of pkg.itemIds) { for (const iid of pkg.itemIds) {
const item = snapshot.session.items[iid]; const item = currentItems[iid];
if (item) allLinks.push({ name: item.fileName, url: item.url }); if (item) allLinks.push({ name: item.fileName, url: item.url });
} }
} else { } else {
const item = snapshot.session.items[id]; const item = currentItems[id];
if (item) allLinks.push({ name: item.fileName, url: item.url }); if (item) allLinks.push({ name: item.fileName, url: item.url });
} }
} }
@ -1638,22 +1644,22 @@ export function App(): ReactElement {
setContextMenu(null); setContextMenu(null);
return; return;
} }
const pkg = snapshot.session.packages[packageId]; const pkg = currentPackages[packageId];
if (!pkg) { return; } if (!pkg) { return; }
if (itemId) { if (itemId) {
const item = snapshot.session.items[itemId]; const item = currentItems[itemId];
if (item) { if (item) {
setLinkPopup({ title: item.fileName, links: [{ name: item.fileName, url: item.url }], isPackage: false }); setLinkPopup({ title: item.fileName, links: [{ name: item.fileName, url: item.url }], isPackage: false });
} }
} else { } else {
const links = pkg.itemIds const links = pkg.itemIds
.map((id) => snapshot.session.items[id]) .map((id) => currentItems[id])
.filter(Boolean) .filter(Boolean)
.map((item) => ({ name: item.fileName, url: item.url })); .map((item) => ({ name: item.fileName, url: item.url }));
setLinkPopup({ title: pkg.name, links, isPackage: true }); setLinkPopup({ title: pkg.name, links, isPackage: true });
} }
setContextMenu(null); setContextMenu(null);
}, [snapshot.session.packages, snapshot.session.items, selectedIds]); }, [selectedIds]);
const schedules = settingsDraft.bandwidthSchedules ?? []; const schedules = settingsDraft.bandwidthSchedules ?? [];
@ -1815,6 +1821,8 @@ export function App(): ReactElement {
if (e.key === "Escape") { if (e.key === "Escape") {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (target.tagName !== "INPUT" && target.tagName !== "TEXTAREA") { if (target.tagName !== "INPUT" && target.tagName !== "TEXTAREA") {
// Don't clear selection if an overlay is open — let the overlay close first
if (document.querySelector(".ctx-menu") || document.querySelector(".modal-backdrop") || document.querySelector(".link-popup-overlay")) return;
if (tabRef.current === "downloads") setSelectedIds(new Set()); if (tabRef.current === "downloads") setSelectedIds(new Set());
else if (tabRef.current === "history") setSelectedHistoryIds(new Set()); else if (tabRef.current === "history") setSelectedHistoryIds(new Set());
} }
@ -1875,35 +1883,43 @@ export function App(): ReactElement {
useEffect(() => { useEffect(() => {
const handler = (e: globalThis.KeyboardEvent): void => { const handler = (e: globalThis.KeyboardEvent): void => {
if (e.ctrlKey && !e.altKey && !e.metaKey) { if (e.ctrlKey && !e.altKey && !e.metaKey) {
const target = e.target as HTMLElement;
const inInput = target.tagName === "INPUT" || target.tagName === "TEXTAREA";
if (e.shiftKey && e.key.toLowerCase() === "r") { if (e.shiftKey && e.key.toLowerCase() === "r") {
if (inInput) return;
e.preventDefault(); e.preventDefault();
void window.rd.restart(); void window.rd.restart();
return; return;
} }
if (!e.shiftKey && e.key.toLowerCase() === "q") { if (!e.shiftKey && e.key.toLowerCase() === "q") {
if (inInput) return;
e.preventDefault(); e.preventDefault();
void window.rd.quit(); void window.rd.quit();
return; return;
} }
if (!e.shiftKey && e.key.toLowerCase() === "l") { if (!e.shiftKey && e.key.toLowerCase() === "l") {
if (inInput) return;
e.preventDefault(); e.preventDefault();
setTab("collector"); setTab("collector");
setOpenMenu(null); setOpenMenu(null);
return; return;
} }
if (!e.shiftKey && e.key.toLowerCase() === "p") { if (!e.shiftKey && e.key.toLowerCase() === "p") {
if (inInput) return;
e.preventDefault(); e.preventDefault();
setTab("settings"); setTab("settings");
setOpenMenu(null); setOpenMenu(null);
return; return;
} }
if (!e.shiftKey && e.key.toLowerCase() === "o") { if (!e.shiftKey && e.key.toLowerCase() === "o") {
if (inInput) return;
e.preventDefault(); e.preventDefault();
setOpenMenu(null); setOpenMenu(null);
void onImportDlc(); void onImportDlc();
return; return;
} }
if (!e.shiftKey && e.key.toLowerCase() === "a") { if (!e.shiftKey && e.key.toLowerCase() === "a") {
if (inInput) return;
if (tabRef.current === "downloads") { if (tabRef.current === "downloads") {
e.preventDefault(); e.preventDefault();
setSelectedIds(new Set(Object.keys(snapshotRef.current.session.packages))); setSelectedIds(new Set(Object.keys(snapshotRef.current.session.packages)));