Release v1.6.25
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1854e6bb17
commit
940346e2f4
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.6.24",
|
"version": "1.6.25",
|
||||||
"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",
|
||||||
|
|||||||
@ -317,7 +317,7 @@ async function runWithConcurrency<T>(items: T[], concurrency: number, worker: (i
|
|||||||
let index = 0;
|
let index = 0;
|
||||||
let firstError: unknown = null;
|
let firstError: unknown = null;
|
||||||
const next = (): T | undefined => {
|
const next = (): T | undefined => {
|
||||||
if (index >= items.length) {
|
if (firstError || index >= items.length) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const item = items[index];
|
const item = items[index];
|
||||||
|
|||||||
@ -2669,6 +2669,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public skipItems(itemIds: string[]): void {
|
public skipItems(itemIds: string[]): void {
|
||||||
|
const affectedPackageIds = new Set<string>();
|
||||||
for (const itemId of itemIds) {
|
for (const itemId of itemIds) {
|
||||||
const item = this.session.items[itemId];
|
const item = this.session.items[itemId];
|
||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
@ -2680,6 +2681,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.retryAfterByItem.delete(itemId);
|
this.retryAfterByItem.delete(itemId);
|
||||||
this.retryStateByItem.delete(itemId);
|
this.retryStateByItem.delete(itemId);
|
||||||
this.recordRunOutcome(itemId, "cancelled");
|
this.recordRunOutcome(itemId, "cancelled");
|
||||||
|
affectedPackageIds.add(item.packageId);
|
||||||
|
}
|
||||||
|
for (const pkgId of affectedPackageIds) {
|
||||||
|
const pkg = this.session.packages[pkgId];
|
||||||
|
if (pkg) this.refreshPackageStatus(pkg);
|
||||||
}
|
}
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
this.emitState();
|
this.emitState();
|
||||||
@ -3328,13 +3334,17 @@ export class DownloadManager extends EventEmitter {
|
|||||||
pkg.itemIds = pkg.itemIds.filter((id) => id !== itemId);
|
pkg.itemIds = pkg.itemIds.filter((id) => id !== itemId);
|
||||||
this.releaseTargetPath(itemId);
|
this.releaseTargetPath(itemId);
|
||||||
this.dropItemContribution(itemId);
|
this.dropItemContribution(itemId);
|
||||||
delete this.session.items[itemId];
|
|
||||||
this.itemCount = Math.max(0, this.itemCount - 1);
|
|
||||||
this.retryAfterByItem.delete(itemId);
|
this.retryAfterByItem.delete(itemId);
|
||||||
|
this.retryStateByItem.delete(itemId);
|
||||||
removed += 1;
|
removed += 1;
|
||||||
}
|
}
|
||||||
if (pkg.itemIds.length === 0) {
|
if (pkg.itemIds.length === 0) {
|
||||||
this.removePackageFromSession(pkgId, []);
|
this.removePackageFromSession(pkgId, completedItemIds);
|
||||||
|
} else {
|
||||||
|
for (const itemId of completedItemIds) {
|
||||||
|
delete this.session.items[itemId];
|
||||||
|
this.itemCount = Math.max(0, this.itemCount - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (policy === "package_done" || policy === "on_start") {
|
} else if (policy === "package_done" || policy === "on_start") {
|
||||||
const allCompleted = pkg.itemIds.every((id) => {
|
const allCompleted = pkg.itemIds.every((id) => {
|
||||||
@ -6301,7 +6311,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
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();
|
const errorAt = nowMs();
|
||||||
for (const entry of hybridItems) {
|
for (const entry of hybridItems) {
|
||||||
if (entry.fullStatus === "Entpacken - Ausstehend" || entry.fullStatus === "Entpacken - Warten auf Parts") {
|
if (isExtractedLabel(entry.fullStatus || "")) continue;
|
||||||
|
if (/^Entpacken\b/i.test(entry.fullStatus || "") || entry.fullStatus === "Entpacken - Ausstehend" || entry.fullStatus === "Entpacken - Warten auf Parts") {
|
||||||
entry.fullStatus = `Entpacken - Error`;
|
entry.fullStatus = `Entpacken - Error`;
|
||||||
entry.updatedAt = errorAt;
|
entry.updatedAt = errorAt;
|
||||||
}
|
}
|
||||||
@ -6838,7 +6849,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const paused = this.session.running && this.session.paused;
|
const paused = this.session.running && this.session.paused;
|
||||||
const currentSpeedBps = paused ? 0 : this.speedBytesLastWindow / SPEED_WINDOW_SECONDS;
|
const currentSpeedBps = !this.session.running || paused ? 0 : this.speedBytesLastWindow / SPEED_WINDOW_SECONDS;
|
||||||
|
|
||||||
let maxSpeed = 0;
|
let maxSpeed = 0;
|
||||||
for (let i = this.speedEventsHead; i < this.speedEvents.length; i += 1) {
|
for (let i = this.speedEventsHead; i < this.speedEvents.length; i += 1) {
|
||||||
|
|||||||
@ -1674,6 +1674,11 @@ export function collectArchiveCleanupTargets(sourceArchivePath: string, director
|
|||||||
return Array.from(targets);
|
return Array.from(targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tar compound archives (.tar.gz, .tar.bz2, .tar.xz, .tgz, .tbz2, .txz)
|
||||||
|
if (/\.(?:tar\.(?:gz|bz2|xz)|tgz|tbz2|txz)$/i.test(fileName)) {
|
||||||
|
return Array.from(targets);
|
||||||
|
}
|
||||||
|
|
||||||
// Generic .NNN split files (HJSplit etc.)
|
// Generic .NNN split files (HJSplit etc.)
|
||||||
const genericSplit = fileName.match(/^(.*)\.(\d{3})$/i);
|
const genericSplit = fileName.match(/^(.*)\.(\d{3})$/i);
|
||||||
if (genericSplit) {
|
if (genericSplit) {
|
||||||
@ -1994,6 +1999,8 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
if (!sig) {
|
if (!sig) {
|
||||||
logger.info(`Generische Split-Datei übersprungen (keine Archiv-Signatur): ${archiveName}`);
|
logger.info(`Generische Split-Datei übersprungen (keine Archiv-Signatur): ${archiveName}`);
|
||||||
extracted += 1;
|
extracted += 1;
|
||||||
|
resumeCompleted.add(archiveResumeKey);
|
||||||
|
extractedArchives.push(archivePath);
|
||||||
clearInterval(pulseTimer);
|
clearInterval(pulseTimer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2127,8 +2134,12 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
try {
|
try {
|
||||||
await extractSingleArchive(queue[idx]);
|
await extractSingleArchive(queue[idx]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isExtractAbortError(String(error))) {
|
const errText = String(error);
|
||||||
abortError = error instanceof Error ? error : new Error(String(error));
|
if (errText.includes("noextractor:skipped")) {
|
||||||
|
break; // handled by noExtractorEncountered flag after the pool
|
||||||
|
}
|
||||||
|
if (isExtractAbortError(errText)) {
|
||||||
|
abortError = error instanceof Error ? error : new Error(errText);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Non-abort errors are already handled inside extractSingleArchive
|
// Non-abort errors are already handled inside extractSingleArchive
|
||||||
|
|||||||
@ -548,6 +548,7 @@ export function loadSession(paths: StoragePaths): SessionState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function saveSession(paths: StoragePaths, session: SessionState): void {
|
export function saveSession(paths: StoragePaths, session: SessionState): void {
|
||||||
|
syncSaveGeneration += 1;
|
||||||
ensureBaseDir(paths.baseDir);
|
ensureBaseDir(paths.baseDir);
|
||||||
if (fs.existsSync(paths.sessionFile)) {
|
if (fs.existsSync(paths.sessionFile)) {
|
||||||
try {
|
try {
|
||||||
@ -569,16 +570,26 @@ export function saveSession(paths: StoragePaths, session: SessionState): void {
|
|||||||
|
|
||||||
let asyncSaveRunning = false;
|
let asyncSaveRunning = false;
|
||||||
let asyncSaveQueued: { paths: StoragePaths; payload: string } | null = null;
|
let asyncSaveQueued: { paths: StoragePaths; payload: string } | null = null;
|
||||||
|
let syncSaveGeneration = 0;
|
||||||
|
|
||||||
async function writeSessionPayload(paths: StoragePaths, payload: string): Promise<void> {
|
async function writeSessionPayload(paths: StoragePaths, payload: string, generation: number): Promise<void> {
|
||||||
await fs.promises.mkdir(paths.baseDir, { recursive: true });
|
await fs.promises.mkdir(paths.baseDir, { recursive: true });
|
||||||
await fsp.copyFile(paths.sessionFile, sessionBackupPath(paths.sessionFile)).catch(() => {});
|
await fsp.copyFile(paths.sessionFile, sessionBackupPath(paths.sessionFile)).catch(() => {});
|
||||||
const tempPath = sessionTempPath(paths.sessionFile, "async");
|
const tempPath = sessionTempPath(paths.sessionFile, "async");
|
||||||
await fsp.writeFile(tempPath, payload, "utf8");
|
await fsp.writeFile(tempPath, payload, "utf8");
|
||||||
|
// If a synchronous save occurred after this async save started, discard the stale write
|
||||||
|
if (generation < syncSaveGeneration) {
|
||||||
|
await fsp.rm(tempPath, { force: true }).catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await fsp.rename(tempPath, paths.sessionFile);
|
await fsp.rename(tempPath, paths.sessionFile);
|
||||||
} catch (renameError: unknown) {
|
} catch (renameError: unknown) {
|
||||||
if (renameError && typeof renameError === "object" && "code" in renameError && (renameError as NodeJS.ErrnoException).code === "EXDEV") {
|
if (renameError && typeof renameError === "object" && "code" in renameError && (renameError as NodeJS.ErrnoException).code === "EXDEV") {
|
||||||
|
if (generation < syncSaveGeneration) {
|
||||||
|
await fsp.rm(tempPath, { force: true }).catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
await fsp.copyFile(tempPath, paths.sessionFile);
|
await fsp.copyFile(tempPath, paths.sessionFile);
|
||||||
await fsp.rm(tempPath, { force: true }).catch(() => {});
|
await fsp.rm(tempPath, { force: true }).catch(() => {});
|
||||||
} else {
|
} else {
|
||||||
@ -593,8 +604,9 @@ async function saveSessionPayloadAsync(paths: StoragePaths, payload: string): Pr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
asyncSaveRunning = true;
|
asyncSaveRunning = true;
|
||||||
|
const gen = syncSaveGeneration;
|
||||||
try {
|
try {
|
||||||
await writeSessionPayload(paths, payload);
|
await writeSessionPayload(paths, payload, gen);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Async Session-Save fehlgeschlagen: ${String(error)}`);
|
logger.error(`Async Session-Save fehlgeschlagen: ${String(error)}`);
|
||||||
} finally {
|
} finally {
|
||||||
@ -610,6 +622,7 @@ async function saveSessionPayloadAsync(paths: StoragePaths, payload: string): Pr
|
|||||||
export function cancelPendingAsyncSaves(): void {
|
export function cancelPendingAsyncSaves(): void {
|
||||||
asyncSaveQueued = null;
|
asyncSaveQueued = null;
|
||||||
asyncSettingsSaveQueued = null;
|
asyncSettingsSaveQueued = null;
|
||||||
|
syncSaveGeneration += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveSessionAsync(paths: StoragePaths, session: SessionState): Promise<void> {
|
export async function saveSessionAsync(paths: StoragePaths, session: SessionState): Promise<void> {
|
||||||
@ -637,7 +650,8 @@ function normalizeHistoryEntry(raw: unknown, index: number): HistoryEntry | null
|
|||||||
completedAt: clampNumber(entry.completedAt, Date.now(), 0, Number.MAX_SAFE_INTEGER),
|
completedAt: clampNumber(entry.completedAt, Date.now(), 0, Number.MAX_SAFE_INTEGER),
|
||||||
durationSeconds: clampNumber(entry.durationSeconds, 0, 0, Number.MAX_SAFE_INTEGER),
|
durationSeconds: clampNumber(entry.durationSeconds, 0, 0, Number.MAX_SAFE_INTEGER),
|
||||||
status: entry.status === "deleted" ? "deleted" : "completed",
|
status: entry.status === "deleted" ? "deleted" : "completed",
|
||||||
outputDir: asText(entry.outputDir)
|
outputDir: asText(entry.outputDir),
|
||||||
|
urls: Array.isArray(entry.urls) ? (entry.urls as unknown[]).map(String).filter(Boolean) : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -473,6 +473,7 @@ export function App(): ReactElement {
|
|||||||
const [settingsDirty, setSettingsDirty] = useState(false);
|
const [settingsDirty, setSettingsDirty] = useState(false);
|
||||||
const settingsDirtyRef = useRef(false);
|
const settingsDirtyRef = useRef(false);
|
||||||
const settingsDraftRevisionRef = useRef(0);
|
const settingsDraftRevisionRef = useRef(0);
|
||||||
|
const panelDirtyRevisionRef = useRef(0);
|
||||||
const latestStateRef = useRef<UiSnapshot | null>(null);
|
const latestStateRef = useRef<UiSnapshot | null>(null);
|
||||||
const snapshotRef = useRef(snapshot);
|
const snapshotRef = useRef(snapshot);
|
||||||
snapshotRef.current = snapshot;
|
snapshotRef.current = snapshot;
|
||||||
@ -645,6 +646,7 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
setSettingsDraft(state.settings);
|
setSettingsDraft(state.settings);
|
||||||
settingsDirtyRef.current = false;
|
settingsDirtyRef.current = false;
|
||||||
|
panelDirtyRevisionRef.current = 0;
|
||||||
setSettingsDirty(false);
|
setSettingsDirty(false);
|
||||||
applyTheme(state.settings.theme);
|
applyTheme(state.settings.theme);
|
||||||
if (state.settings.autoUpdateCheck) {
|
if (state.settings.autoUpdateCheck) {
|
||||||
@ -1044,6 +1046,7 @@ export function App(): ReactElement {
|
|||||||
if (settingsDraftRevisionRef.current === revisionAtStart) {
|
if (settingsDraftRevisionRef.current === revisionAtStart) {
|
||||||
setSettingsDraft(result);
|
setSettingsDraft(result);
|
||||||
settingsDirtyRef.current = false;
|
settingsDirtyRef.current = false;
|
||||||
|
panelDirtyRevisionRef.current = 0;
|
||||||
setSettingsDirty(false);
|
setSettingsDirty(false);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@ -1290,18 +1293,21 @@ export function App(): ReactElement {
|
|||||||
|
|
||||||
const setBool = (key: keyof AppSettings, value: boolean): void => {
|
const setBool = (key: keyof AppSettings, value: boolean): void => {
|
||||||
settingsDraftRevisionRef.current += 1;
|
settingsDraftRevisionRef.current += 1;
|
||||||
|
panelDirtyRevisionRef.current += 1;
|
||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDirty(true);
|
setSettingsDirty(true);
|
||||||
setSettingsDraft((prev) => ({ ...prev, [key]: value }));
|
setSettingsDraft((prev) => ({ ...prev, [key]: value }));
|
||||||
};
|
};
|
||||||
const setText = (key: keyof AppSettings, value: string): void => {
|
const setText = (key: keyof AppSettings, value: string): void => {
|
||||||
settingsDraftRevisionRef.current += 1;
|
settingsDraftRevisionRef.current += 1;
|
||||||
|
panelDirtyRevisionRef.current += 1;
|
||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDirty(true);
|
setSettingsDirty(true);
|
||||||
setSettingsDraft((prev) => ({ ...prev, [key]: value }));
|
setSettingsDraft((prev) => ({ ...prev, [key]: value }));
|
||||||
};
|
};
|
||||||
const setNum = (key: keyof AppSettings, value: number): void => {
|
const setNum = (key: keyof AppSettings, value: number): void => {
|
||||||
settingsDraftRevisionRef.current += 1;
|
settingsDraftRevisionRef.current += 1;
|
||||||
|
panelDirtyRevisionRef.current += 1;
|
||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDirty(true);
|
setSettingsDirty(true);
|
||||||
setSettingsDraft((prev) => ({ ...prev, [key]: value }));
|
setSettingsDraft((prev) => ({ ...prev, [key]: value }));
|
||||||
@ -1309,6 +1315,7 @@ export function App(): ReactElement {
|
|||||||
const setSpeedLimitMbps = (value: number): void => {
|
const setSpeedLimitMbps = (value: number): void => {
|
||||||
const mbps = Number.isFinite(value) ? Math.max(0, value) : 0;
|
const mbps = Number.isFinite(value) ? Math.max(0, value) : 0;
|
||||||
settingsDraftRevisionRef.current += 1;
|
settingsDraftRevisionRef.current += 1;
|
||||||
|
panelDirtyRevisionRef.current += 1;
|
||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDirty(true);
|
setSettingsDirty(true);
|
||||||
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: Math.floor(mbps * 1024) }));
|
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: Math.floor(mbps * 1024) }));
|
||||||
@ -1434,16 +1441,20 @@ export function App(): ReactElement {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onPackageFinishEdit = useCallback((packageId: string, currentName: string, nextName: string): void => {
|
const onPackageFinishEdit = useCallback((packageId: string, currentName: string, nextName: string): void => {
|
||||||
|
let shouldRename = false;
|
||||||
setEditingPackageId((prev) => {
|
setEditingPackageId((prev) => {
|
||||||
if (prev !== packageId) return prev; // already finished (e.g. blur after Enter key)
|
if (prev !== packageId) return prev; // already finished (e.g. blur after Enter key)
|
||||||
|
shouldRename = true;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (shouldRename) {
|
||||||
const normalized = nextName.trim();
|
const normalized = nextName.trim();
|
||||||
if (normalized && normalized !== currentName.trim()) {
|
if (normalized && normalized !== currentName.trim()) {
|
||||||
void window.rd.renamePackage(packageId, normalized).catch((error) => {
|
void window.rd.renamePackage(packageId, normalized).catch((error) => {
|
||||||
showToast(`Umbenennen fehlgeschlagen: ${String(error)}`, 2400);
|
showToast(`Umbenennen fehlgeschlagen: ${String(error)}`, 2400);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
});
|
|
||||||
}, [showToast]);
|
}, [showToast]);
|
||||||
|
|
||||||
const onPackageToggleCollapse = useCallback((packageId: string): void => {
|
const onPackageToggleCollapse = useCallback((packageId: string): void => {
|
||||||
@ -1691,6 +1702,7 @@ export function App(): ReactElement {
|
|||||||
|
|
||||||
const addSchedule = (): void => {
|
const addSchedule = (): void => {
|
||||||
settingsDraftRevisionRef.current += 1;
|
settingsDraftRevisionRef.current += 1;
|
||||||
|
panelDirtyRevisionRef.current += 1;
|
||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDirty(true);
|
setSettingsDirty(true);
|
||||||
setSettingsDraft((prev) => ({
|
setSettingsDraft((prev) => ({
|
||||||
@ -1700,6 +1712,7 @@ export function App(): ReactElement {
|
|||||||
};
|
};
|
||||||
const removeSchedule = (idx: number): void => {
|
const removeSchedule = (idx: number): void => {
|
||||||
settingsDraftRevisionRef.current += 1;
|
settingsDraftRevisionRef.current += 1;
|
||||||
|
panelDirtyRevisionRef.current += 1;
|
||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDirty(true);
|
setSettingsDirty(true);
|
||||||
setSettingsDraft((prev) => ({
|
setSettingsDraft((prev) => ({
|
||||||
@ -1709,6 +1722,7 @@ export function App(): ReactElement {
|
|||||||
};
|
};
|
||||||
const updateSchedule = (idx: number, field: keyof BandwidthScheduleEntry, value: number | boolean): void => {
|
const updateSchedule = (idx: number, field: keyof BandwidthScheduleEntry, value: number | boolean): void => {
|
||||||
settingsDraftRevisionRef.current += 1;
|
settingsDraftRevisionRef.current += 1;
|
||||||
|
panelDirtyRevisionRef.current += 1;
|
||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDirty(true);
|
setSettingsDirty(true);
|
||||||
setSettingsDraft((prev) => ({
|
setSettingsDraft((prev) => ({
|
||||||
@ -2086,7 +2100,7 @@ export function App(): ReactElement {
|
|||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
const rev = ++settingsDraftRevisionRef.current;
|
const rev = ++settingsDraftRevisionRef.current;
|
||||||
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
||||||
void window.rd.updateSettings({ maxParallel: val }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; });
|
void window.rd.updateSettings({ maxParallel: val }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) settingsDirtyRef.current = false; });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="menu-spinner-arrows">
|
<div className="menu-spinner-arrows">
|
||||||
@ -2095,14 +2109,14 @@ export function App(): ReactElement {
|
|||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
const rev = ++settingsDraftRevisionRef.current;
|
const rev = ++settingsDraftRevisionRef.current;
|
||||||
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
||||||
void window.rd.updateSettings({ maxParallel: val }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; });
|
void window.rd.updateSettings({ maxParallel: val }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) 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;
|
settingsDirtyRef.current = true;
|
||||||
const rev = ++settingsDraftRevisionRef.current;
|
const rev = ++settingsDraftRevisionRef.current;
|
||||||
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
setSettingsDraft((prev) => ({ ...prev, maxParallel: val }));
|
||||||
void window.rd.updateSettings({ maxParallel: val }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; });
|
void window.rd.updateSettings({ maxParallel: val }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) settingsDirtyRef.current = false; });
|
||||||
}}>▼</button>
|
}}>▼</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -2117,7 +2131,7 @@ export function App(): ReactElement {
|
|||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
const rev = ++settingsDraftRevisionRef.current;
|
const rev = ++settingsDraftRevisionRef.current;
|
||||||
setSettingsDraft((prev) => ({ ...prev, speedLimitEnabled: next }));
|
setSettingsDraft((prev) => ({ ...prev, speedLimitEnabled: next }));
|
||||||
void window.rd.updateSettings({ speedLimitEnabled: next }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; });
|
void window.rd.updateSettings({ speedLimitEnabled: next }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) settingsDirtyRef.current = false; });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={`menu-spinner${!settingsDraft.speedLimitEnabled ? " disabled" : ""}`}>
|
<div className={`menu-spinner${!settingsDraft.speedLimitEnabled ? " disabled" : ""}`}>
|
||||||
@ -2138,7 +2152,7 @@ export function App(): ReactElement {
|
|||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
const rev = ++settingsDraftRevisionRef.current;
|
const rev = ++settingsDraftRevisionRef.current;
|
||||||
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: kbps }));
|
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: kbps }));
|
||||||
void window.rd.updateSettings({ speedLimitKbps: kbps }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; });
|
void window.rd.updateSettings({ speedLimitKbps: kbps }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) settingsDirtyRef.current = false; });
|
||||||
setSpeedLimitInput(formatMbpsInputFromKbps(kbps));
|
setSpeedLimitInput(formatMbpsInputFromKbps(kbps));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -2149,7 +2163,7 @@ export function App(): ReactElement {
|
|||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
const rev = ++settingsDraftRevisionRef.current;
|
const rev = ++settingsDraftRevisionRef.current;
|
||||||
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: next }));
|
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: next }));
|
||||||
void window.rd.updateSettings({ speedLimitKbps: next }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; });
|
void window.rd.updateSettings({ speedLimitKbps: next }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) settingsDirtyRef.current = false; });
|
||||||
setSpeedLimitInput(formatMbpsInputFromKbps(next));
|
setSpeedLimitInput(formatMbpsInputFromKbps(next));
|
||||||
}}>▲</button>
|
}}>▲</button>
|
||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
@ -2158,7 +2172,7 @@ export function App(): ReactElement {
|
|||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
const rev = ++settingsDraftRevisionRef.current;
|
const rev = ++settingsDraftRevisionRef.current;
|
||||||
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: next }));
|
setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: next }));
|
||||||
void window.rd.updateSettings({ speedLimitKbps: next }).finally(() => { if (settingsDraftRevisionRef.current === rev) settingsDirtyRef.current = false; });
|
void window.rd.updateSettings({ speedLimitKbps: next }).finally(() => { if (settingsDraftRevisionRef.current === rev && panelDirtyRevisionRef.current === 0) settingsDirtyRef.current = false; });
|
||||||
setSpeedLimitInput(formatMbpsInputFromKbps(next));
|
setSpeedLimitInput(formatMbpsInputFromKbps(next));
|
||||||
}}>▼</button>
|
}}>▼</button>
|
||||||
</div>
|
</div>
|
||||||
@ -2534,7 +2548,7 @@ export function App(): ReactElement {
|
|||||||
{tab === "statistics" && (
|
{tab === "statistics" && (
|
||||||
<section className="statistics-view">
|
<section className="statistics-view">
|
||||||
<article className="card stats-overview">
|
<article className="card stats-overview">
|
||||||
<h3>Session-Ubersicht</h3>
|
<h3>Session-Übersicht</h3>
|
||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
<div className="stat-item">
|
<div className="stat-item">
|
||||||
<span className="stat-label">Aktuelle Geschwindigkeit</span>
|
<span className="stat-label">Aktuelle Geschwindigkeit</span>
|
||||||
@ -2648,6 +2662,7 @@ export function App(): ReactElement {
|
|||||||
<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;
|
||||||
|
panelDirtyRevisionRef.current += 1;
|
||||||
settingsDirtyRef.current = true;
|
settingsDirtyRef.current = true;
|
||||||
setSettingsDirty(true);
|
setSettingsDirty(true);
|
||||||
setSettingsDraft((prev) => ({ ...prev, theme: next as AppTheme }));
|
setSettingsDraft((prev) => ({ ...prev, theme: next as AppTheme }));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user