Release v1.6.4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
693f7b482a
commit
7446e07a8c
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.6.3",
|
"version": "1.6.4",
|
||||||
"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",
|
||||||
|
|||||||
@ -417,6 +417,7 @@ async function resolveRapidgatorFilename(link: string, signal?: AbortSignal): Pr
|
|||||||
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
|
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
try { await response.body?.cancel(); } catch { /* drain socket */ }
|
||||||
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES + 2) {
|
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES + 2) {
|
||||||
await sleepWithSignal(retryDelayForResponse(response, attempt), signal);
|
await sleepWithSignal(retryDelayForResponse(response, attempt), signal);
|
||||||
continue;
|
continue;
|
||||||
@ -432,9 +433,11 @@ async function resolveRapidgatorFilename(link: string, signal?: AbortSignal): Pr
|
|||||||
&& !contentType.includes("text/plain")
|
&& !contentType.includes("text/plain")
|
||||||
&& !contentType.includes("text/xml")
|
&& !contentType.includes("text/xml")
|
||||||
&& !contentType.includes("application/xml")) {
|
&& !contentType.includes("application/xml")) {
|
||||||
|
try { await response.body?.cancel(); } catch { /* drain socket */ }
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (!contentType && Number.isFinite(contentLength) && contentLength > RAPIDGATOR_SCAN_MAX_BYTES) {
|
if (!contentType && Number.isFinite(contentLength) && contentLength > RAPIDGATOR_SCAN_MAX_BYTES) {
|
||||||
|
try { await response.body?.cancel(); } catch { /* drain socket */ }
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,10 +551,12 @@ export async function checkRapidgatorOnline(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
|
try { await response.body?.cancel(); } catch { /* drain socket */ }
|
||||||
return { online: false, fileName: "", fileSize: null };
|
return { online: false, fileName: "", fileSize: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
try { await response.body?.cancel(); } catch { /* drain socket */ }
|
||||||
if (shouldRetryStatus(response.status) && attempt <= REQUEST_RETRIES) {
|
if (shouldRetryStatus(response.status) && attempt <= REQUEST_RETRIES) {
|
||||||
await sleepWithSignal(retryDelayForResponse(response, attempt), signal);
|
await sleepWithSignal(retryDelayForResponse(response, attempt), signal);
|
||||||
continue;
|
continue;
|
||||||
@ -561,6 +566,7 @@ export async function checkRapidgatorOnline(
|
|||||||
|
|
||||||
const finalUrl = response.url || link;
|
const finalUrl = response.url || link;
|
||||||
if (!finalUrl.includes(fileId)) {
|
if (!finalUrl.includes(fileId)) {
|
||||||
|
try { await response.body?.cancel(); } catch { /* drain socket */ }
|
||||||
return { online: false, fileName: "", fileSize: null };
|
return { online: false, fileName: "", fileSize: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1648,6 +1648,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
for (const item of Object.values(this.session.items)) {
|
for (const item of Object.values(this.session.items)) {
|
||||||
if (item.status !== "queued") continue;
|
if (item.status !== "queued") continue;
|
||||||
if (item.onlineStatus) continue; // already checked
|
if (item.onlineStatus) continue; // already checked
|
||||||
|
try {
|
||||||
|
const host = new URL(item.url).hostname.toLowerCase();
|
||||||
|
if (host !== "rapidgator.net" && !host.endsWith(".rapidgator.net") && host !== "rg.to" && !host.endsWith(".rg.to")) continue;
|
||||||
|
} catch { continue; }
|
||||||
uncheckedIds.push(item.id);
|
uncheckedIds.push(item.id);
|
||||||
}
|
}
|
||||||
if (uncheckedIds.length > 0) {
|
if (uncheckedIds.length > 0) {
|
||||||
@ -2659,10 +2663,12 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.runOutcomes.clear();
|
this.runOutcomes.clear();
|
||||||
this.runCompletedPackages.clear();
|
this.runCompletedPackages.clear();
|
||||||
this.retryAfterByItem.clear();
|
this.retryAfterByItem.clear();
|
||||||
|
this.retryStateByItem.clear();
|
||||||
this.session.running = true;
|
this.session.running = true;
|
||||||
this.session.paused = false;
|
this.session.paused = false;
|
||||||
this.session.runStartedAt = nowMs();
|
this.session.runStartedAt = nowMs();
|
||||||
this.session.totalDownloadedBytes = 0;
|
this.session.totalDownloadedBytes = 0;
|
||||||
|
this.sessionDownloadedBytes = 0;
|
||||||
this.session.summaryText = "";
|
this.session.summaryText = "";
|
||||||
this.session.reconnectUntil = 0;
|
this.session.reconnectUntil = 0;
|
||||||
this.session.reconnectReason = "";
|
this.session.reconnectReason = "";
|
||||||
@ -2760,10 +2766,12 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.runOutcomes.clear();
|
this.runOutcomes.clear();
|
||||||
this.runCompletedPackages.clear();
|
this.runCompletedPackages.clear();
|
||||||
this.retryAfterByItem.clear();
|
this.retryAfterByItem.clear();
|
||||||
|
this.retryStateByItem.clear();
|
||||||
this.session.running = true;
|
this.session.running = true;
|
||||||
this.session.paused = false;
|
this.session.paused = false;
|
||||||
this.session.runStartedAt = nowMs();
|
this.session.runStartedAt = nowMs();
|
||||||
this.session.totalDownloadedBytes = 0;
|
this.session.totalDownloadedBytes = 0;
|
||||||
|
this.sessionDownloadedBytes = 0;
|
||||||
this.session.summaryText = "";
|
this.session.summaryText = "";
|
||||||
this.session.reconnectUntil = 0;
|
this.session.reconnectUntil = 0;
|
||||||
this.session.reconnectReason = "";
|
this.session.reconnectReason = "";
|
||||||
@ -2930,6 +2938,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.abortPostProcessing("stop");
|
this.abortPostProcessing("stop");
|
||||||
for (const waiter of this.packagePostProcessWaiters) { waiter.resolve(); }
|
for (const waiter of this.packagePostProcessWaiters) { waiter.resolve(); }
|
||||||
this.packagePostProcessWaiters = [];
|
this.packagePostProcessWaiters = [];
|
||||||
|
this.packagePostProcessActive = 0;
|
||||||
for (const active of this.activeTasks.values()) {
|
for (const active of this.activeTasks.values()) {
|
||||||
active.abortReason = "stop";
|
active.abortReason = "stop";
|
||||||
active.abortController.abort("stop");
|
active.abortController.abort("stop");
|
||||||
@ -3428,9 +3437,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.error(`claimTargetPath: Limit erreicht für ${preferredPath}`);
|
logger.error(`claimTargetPath: Limit erreicht für ${preferredPath}`);
|
||||||
this.reservedTargetPaths.set(pathKey(preferredPath), itemId);
|
const fallbackPath = path.join(parsed.dir, `${parsed.name} (${Date.now()})${parsed.ext}`);
|
||||||
this.claimedTargetPathByItem.set(itemId, preferredPath);
|
this.reservedTargetPaths.set(pathKey(fallbackPath), itemId);
|
||||||
return preferredPath;
|
this.claimedTargetPathByItem.set(itemId, fallbackPath);
|
||||||
|
return fallbackPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private releaseTargetPath(itemId: string): void {
|
private releaseTargetPath(itemId: string): void {
|
||||||
@ -4433,6 +4443,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
this.releaseTargetPath(item.id);
|
this.releaseTargetPath(item.id);
|
||||||
|
this.dropItemContribution(item.id);
|
||||||
item.downloadedBytes = 0;
|
item.downloadedBytes = 0;
|
||||||
item.progressPercent = 0;
|
item.progressPercent = 0;
|
||||||
item.totalBytes = (item.totalBytes || 0) > 0 ? item.totalBytes : null;
|
item.totalBytes = (item.totalBytes || 0) > 0 ? item.totalBytes : null;
|
||||||
@ -4594,11 +4605,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const shouldFreshRetry = !active.freshRetryUsed && isFetchFailure(errorText);
|
const shouldFreshRetry = !active.freshRetryUsed && isFetchFailure(errorText);
|
||||||
const isHttp416 = /(^|\D)416(\D|$)/.test(errorText);
|
const isHttp416 = /(^|\D)416(\D|$)/.test(errorText);
|
||||||
if (isHttp416) {
|
if (isHttp416) {
|
||||||
|
if (claimedTargetPath) {
|
||||||
try {
|
try {
|
||||||
fs.rmSync(item.targetPath, { force: true });
|
fs.rmSync(claimedTargetPath, { force: true });
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.releaseTargetPath(item.id);
|
this.releaseTargetPath(item.id);
|
||||||
item.downloadedBytes = 0;
|
item.downloadedBytes = 0;
|
||||||
item.totalBytes = null;
|
item.totalBytes = null;
|
||||||
@ -4619,11 +4632,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
active.freshRetryUsed = true;
|
active.freshRetryUsed = true;
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
logger.warn(`Netzwerkfehler: item=${item.fileName || item.id}, fresh retry, error=${errorText}, provider=${item.provider || "?"}`);
|
logger.warn(`Netzwerkfehler: item=${item.fileName || item.id}, fresh retry, error=${errorText}, provider=${item.provider || "?"}`);
|
||||||
|
if (claimedTargetPath) {
|
||||||
try {
|
try {
|
||||||
fs.rmSync(item.targetPath, { force: true });
|
fs.rmSync(claimedTargetPath, { force: true });
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.releaseTargetPath(item.id);
|
this.releaseTargetPath(item.id);
|
||||||
this.queueRetry(item, active, 300, "Netzwerkfehler erkannt, frischer Retry");
|
this.queueRetry(item, active, 300, "Netzwerkfehler erkannt, frischer Retry");
|
||||||
item.lastError = "";
|
item.lastError = "";
|
||||||
@ -4854,6 +4869,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
if (this.settings.autoReconnect && [429, 503].includes(response.status)) {
|
if (this.settings.autoReconnect && [429, 503].includes(response.status)) {
|
||||||
this.requestReconnect(`HTTP ${response.status}`);
|
this.requestReconnect(`HTTP ${response.status}`);
|
||||||
|
throw new Error(lastError);
|
||||||
}
|
}
|
||||||
if (attempt < maxAttempts) {
|
if (attempt < maxAttempts) {
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
@ -5990,7 +6006,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const status = entry.fullStatus || "";
|
const status = entry.fullStatus || "";
|
||||||
if (/^Entpacken\b/i.test(status) || /^Fertig\b/i.test(status)) {
|
if (/^Entpacken\b/i.test(status)) {
|
||||||
if (result.extracted > 0 && result.failed === 0) {
|
if (result.extracted > 0 && result.failed === 0) {
|
||||||
entry.fullStatus = formatExtractDone(nowMs() - hybridExtractStartMs);
|
entry.fullStatus = formatExtractDone(nowMs() - hybridExtractStartMs);
|
||||||
} else {
|
} else {
|
||||||
@ -6006,6 +6022,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.warn(`Hybrid-Extract Fehler: pkg=${pkg.name}, reason=${compactErrorText(error)}`);
|
logger.warn(`Hybrid-Extract Fehler: pkg=${pkg.name}, reason=${compactErrorText(error)}`);
|
||||||
|
const errorAt = nowMs();
|
||||||
|
for (const entry of hybridItems) {
|
||||||
|
if (entry.fullStatus === "Entpacken - Ausstehend" || entry.fullStatus === "Entpacken - Warten auf Parts") {
|
||||||
|
entry.fullStatus = `Entpacken - Error`;
|
||||||
|
entry.updatedAt = errorAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6383,11 +6406,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allCompleted = pkg.itemIds.every((itemId) => {
|
const allDone = pkg.itemIds.every((itemId) => {
|
||||||
const item = this.session.items[itemId];
|
const item = this.session.items[itemId];
|
||||||
return !item || item.status === "completed";
|
return !item || item.status === "completed" || item.status === "cancelled" || item.status === "failed";
|
||||||
});
|
});
|
||||||
if (!allCompleted) {
|
if (!allDone) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6439,7 +6462,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (policy === "package_done") {
|
if (policy === "package_done") {
|
||||||
const hasOpen = pkg.itemIds.some((id) => {
|
const hasOpen = pkg.itemIds.some((id) => {
|
||||||
const item = this.session.items[id];
|
const item = this.session.items[id];
|
||||||
return item != null && item.status !== "completed";
|
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 ALL items are extracted, not just downloaded
|
||||||
@ -6550,9 +6573,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
completedDownloads += 1;
|
completedDownloads += 1;
|
||||||
} else if (item.status === "failed") {
|
} else if (item.status === "failed") {
|
||||||
failedDownloads += 1;
|
failedDownloads += 1;
|
||||||
} else if (item.status === "downloading" || item.status === "validating") {
|
} else if (item.status === "downloading" || item.status === "validating" || item.status === "integrity_check") {
|
||||||
activeDownloads += 1;
|
activeDownloads += 1;
|
||||||
} else if (item.status === "queued" || item.status === "reconnect_wait") {
|
} else if (item.status === "queued" || item.status === "reconnect_wait" || item.status === "paused") {
|
||||||
queuedDownloads += 1;
|
queuedDownloads += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1219,9 +1219,10 @@ async function resolveExtractorCommandInternal(): Promise<string> {
|
|||||||
if (isAbsoluteCommand(command) && !fs.existsSync(command)) {
|
if (isAbsoluteCommand(command) && !fs.existsSync(command)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const probeArgs = command.toLowerCase().includes("winrar") ? ["-?"] : ["?"];
|
const lower = command.toLowerCase();
|
||||||
|
const probeArgs = (lower.includes("winrar") || lower.includes("unrar")) ? ["-?"] : ["?"];
|
||||||
const probe = await runExtractCommand(command, probeArgs, undefined, undefined, EXTRACTOR_PROBE_TIMEOUT_MS);
|
const probe = await runExtractCommand(command, probeArgs, undefined, undefined, EXTRACTOR_PROBE_TIMEOUT_MS);
|
||||||
if (probe.ok) {
|
if (probe.ok || (!probe.missingCommand && !probe.timedOut)) {
|
||||||
resolvedExtractorCommand = command;
|
resolvedExtractorCommand = command;
|
||||||
resolveFailureReason = "";
|
resolveFailureReason = "";
|
||||||
resolveFailureAt = 0;
|
resolveFailureAt = 0;
|
||||||
|
|||||||
@ -117,7 +117,7 @@ function formatHoster(item: DownloadItem): string {
|
|||||||
const hoster = extractHoster(item.url);
|
const hoster = extractHoster(item.url);
|
||||||
const label = hoster || "-";
|
const label = hoster || "-";
|
||||||
if (item.provider) {
|
if (item.provider) {
|
||||||
return `${label} via. ${providerLabels[item.provider]}`;
|
return `${label} via ${providerLabels[item.provider]}`;
|
||||||
}
|
}
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
@ -2010,20 +2010,23 @@ export function App(): ReactElement {
|
|||||||
value={settingsDraft.maxParallel}
|
value={settingsDraft.maxParallel}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = Math.max(1, Math.min(50, Number(e.target.value) || 1));
|
const val = Math.max(1, Math.min(50, Number(e.target.value) || 1));
|
||||||
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
||||||
void window.rd.updateSettings({ maxParallel: val });
|
void window.rd.updateSettings({ maxParallel: val }).finally(() => { settingsDirtyRef.current = false; });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="menu-spinner-arrows">
|
<div className="menu-spinner-arrows">
|
||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
const val = Math.min(50, settingsDraft.maxParallel + 1);
|
const val = Math.min(50, settingsDraft.maxParallel + 1);
|
||||||
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
||||||
void window.rd.updateSettings({ maxParallel: val });
|
void window.rd.updateSettings({ maxParallel: val }).finally(() => { settingsDirtyRef.current = false; });
|
||||||
}}>▲</button>
|
}}>▲</button>
|
||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
const val = Math.max(1, settingsDraft.maxParallel - 1);
|
const val = Math.max(1, settingsDraft.maxParallel - 1);
|
||||||
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
||||||
void window.rd.updateSettings({ maxParallel: val });
|
void window.rd.updateSettings({ maxParallel: val }).finally(() => { settingsDirtyRef.current = false; });
|
||||||
}}>▼</button>
|
}}>▼</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -2035,8 +2038,9 @@ export function App(): ReactElement {
|
|||||||
checked={settingsDraft.speedLimitEnabled}
|
checked={settingsDraft.speedLimitEnabled}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const next = e.target.checked;
|
const next = e.target.checked;
|
||||||
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDraft((prev) => ({ ...prev, speedLimitEnabled: next }));
|
setSettingsDraft((prev) => ({ ...prev, speedLimitEnabled: next }));
|
||||||
void window.rd.updateSettings({ speedLimitEnabled: next });
|
void window.rd.updateSettings({ speedLimitEnabled: next }).finally(() => { settingsDirtyRef.current = false; });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={`menu-spinner${!settingsDraft.speedLimitEnabled ? " disabled" : ""}`}>
|
<div className={`menu-spinner${!settingsDraft.speedLimitEnabled ? " disabled" : ""}`}>
|
||||||
@ -2054,8 +2058,9 @@ export function App(): ReactElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const kbps = Math.floor(parsed * 1024);
|
const kbps = Math.floor(parsed * 1024);
|
||||||
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: kbps }));
|
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: kbps }));
|
||||||
void window.rd.updateSettings({ speedLimitKbps: kbps });
|
void window.rd.updateSettings({ speedLimitKbps: kbps }).finally(() => { settingsDirtyRef.current = false; });
|
||||||
setSpeedLimitInput(formatMbpsInputFromKbps(kbps));
|
setSpeedLimitInput(formatMbpsInputFromKbps(kbps));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -2063,15 +2068,17 @@ export function App(): ReactElement {
|
|||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
const cur = (settingsDraft.speedLimitKbps || 0) / 1024;
|
const cur = (settingsDraft.speedLimitKbps || 0) / 1024;
|
||||||
const next = Math.floor((cur + 1) * 1024);
|
const next = Math.floor((cur + 1) * 1024);
|
||||||
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: next }));
|
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: next }));
|
||||||
void window.rd.updateSettings({ speedLimitKbps: next });
|
void window.rd.updateSettings({ speedLimitKbps: next }).finally(() => { settingsDirtyRef.current = false; });
|
||||||
setSpeedLimitInput(formatMbpsInputFromKbps(next));
|
setSpeedLimitInput(formatMbpsInputFromKbps(next));
|
||||||
}}>▲</button>
|
}}>▲</button>
|
||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
const cur = (settingsDraft.speedLimitKbps || 0) / 1024;
|
const cur = (settingsDraft.speedLimitKbps || 0) / 1024;
|
||||||
const next = Math.max(0, Math.floor((cur - 1) * 1024));
|
const next = Math.max(0, Math.floor((cur - 1) * 1024));
|
||||||
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: next }));
|
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: next }));
|
||||||
void window.rd.updateSettings({ speedLimitKbps: next });
|
void window.rd.updateSettings({ speedLimitKbps: next }).finally(() => { settingsDirtyRef.current = false; });
|
||||||
setSpeedLimitInput(formatMbpsInputFromKbps(next));
|
setSpeedLimitInput(formatMbpsInputFromKbps(next));
|
||||||
}}>▼</button>
|
}}>▼</button>
|
||||||
</div>
|
</div>
|
||||||
@ -2324,6 +2331,8 @@ export function App(): ReactElement {
|
|||||||
void Promise.all([...idSet].map(id => window.rd.removeHistoryEntry(id))).then(() => {
|
void Promise.all([...idSet].map(id => window.rd.removeHistoryEntry(id))).then(() => {
|
||||||
setHistoryEntries((prev) => prev.filter((e) => !idSet.has(e.id)));
|
setHistoryEntries((prev) => prev.filter((e) => !idSet.has(e.id)));
|
||||||
setSelectedHistoryIds(new Set());
|
setSelectedHistoryIds(new Set());
|
||||||
|
}).catch(() => {
|
||||||
|
void window.rd.getHistory().then((entries) => { setHistoryEntries(entries); setSelectedHistoryIds(new Set()); }).catch(() => {});
|
||||||
});
|
});
|
||||||
}}>Ausgewählte entfernen ({selectedHistoryIds.size})</button>
|
}}>Ausgewählte entfernen ({selectedHistoryIds.size})</button>
|
||||||
)}
|
)}
|
||||||
@ -2992,6 +3001,8 @@ export function App(): ReactElement {
|
|||||||
void Promise.all(ids.map(id => window.rd.removeHistoryEntry(id))).then(() => {
|
void Promise.all(ids.map(id => window.rd.removeHistoryEntry(id))).then(() => {
|
||||||
setHistoryEntries((prev) => prev.filter((e) => !selectedHistoryIds.has(e.id)));
|
setHistoryEntries((prev) => prev.filter((e) => !selectedHistoryIds.has(e.id)));
|
||||||
setSelectedHistoryIds(new Set());
|
setSelectedHistoryIds(new Set());
|
||||||
|
}).catch(() => {
|
||||||
|
void window.rd.getHistory().then((entries) => { setHistoryEntries(entries); setSelectedHistoryIds(new Set()); }).catch(() => {});
|
||||||
});
|
});
|
||||||
setHistoryCtxMenu(null);
|
setHistoryCtxMenu(null);
|
||||||
};
|
};
|
||||||
@ -3216,7 +3227,7 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
|
|||||||
</header>
|
</header>
|
||||||
<div className="progress">
|
<div className="progress">
|
||||||
<div className="progress-dl" style={{ width: `${dlProgress}%` }} />
|
<div className="progress-dl" style={{ width: `${dlProgress}%` }} />
|
||||||
{extracting && <div className="progress-ex" style={{ width: `${exProgress}%` }} />}
|
{useExtractSplit && <div className="progress-ex" style={{ width: `${exProgress}%` }} />}
|
||||||
</div>
|
</div>
|
||||||
{!collapsed && items.map((item) => (
|
{!collapsed && items.map((item) => (
|
||||||
<div key={item.id} className={`item-row${selectedIds.has(item.id) ? " item-selected" : ""}`} style={{ gridTemplateColumns: gridTemplate }} onClick={(e) => { e.stopPropagation(); onSelect(item.id, e.ctrlKey); }} onMouseDown={(e) => { e.stopPropagation(); onSelectMouseDown(item.id, e); }} onMouseEnter={() => onSelectMouseEnter(item.id)} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(pkg.id, item.id, e.clientX, e.clientY); }}>
|
<div key={item.id} className={`item-row${selectedIds.has(item.id) ? " item-selected" : ""}`} style={{ gridTemplateColumns: gridTemplate }} onClick={(e) => { e.stopPropagation(); onSelect(item.id, e.ctrlKey); }} onMouseDown={(e) => { e.stopPropagation(); onSelectMouseDown(item.id, e); }} onMouseEnter={() => onSelectMouseEnter(item.id)} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(pkg.id, item.id, e.clientX, e.clientY); }}>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user