Release v1.6.23
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9cceaacd14
commit
26b2ef0abb
@ -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",
|
||||||
|
|||||||
@ -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)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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)));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user