Unified package/total speed calculation, confirm dialog for context menu delete, show progress bar when collapsed
- Track packageId in speed events so package speed uses same 3-second window as global speed (fixes mismatch between package and status bar) - Add packageSpeedBps to UiSnapshot, computed from speed events - Context menu delete actions now respect confirmDeleteSelection setting - Progress bar visible even when package is collapsed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0d1deadb6f
commit
670c2f1ff5
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.97",
|
"version": "1.4.98",
|
||||||
"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",
|
||||||
|
|||||||
@ -718,7 +718,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private persistTimer: NodeJS.Timeout | null = null;
|
private persistTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
private speedEvents: Array<{ at: number; bytes: number }> = [];
|
private speedEvents: Array<{ at: number; bytes: number; pid: string }> = [];
|
||||||
|
|
||||||
private summary: DownloadSummary | null = null;
|
private summary: DownloadSummary | null = null;
|
||||||
|
|
||||||
@ -872,7 +872,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
canStop: this.session.running,
|
canStop: this.session.running,
|
||||||
canPause: this.session.running,
|
canPause: this.session.running,
|
||||||
clipboardActive: this.settings.clipboardWatch,
|
clipboardActive: this.settings.clipboardWatch,
|
||||||
reconnectSeconds: Math.ceil(reconnectMs / 1000)
|
reconnectSeconds: Math.ceil(reconnectMs / 1000),
|
||||||
|
packageSpeedBps: paused ? {} : Object.fromEntries(
|
||||||
|
[...this.speedBytesPerPackage].map(([pid, bytes]) => [pid, Math.floor(bytes / 3)])
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -932,6 +935,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.speedEvents = [];
|
this.speedEvents = [];
|
||||||
this.speedEventsHead = 0;
|
this.speedEventsHead = 0;
|
||||||
this.speedBytesLastWindow = 0;
|
this.speedBytesLastWindow = 0;
|
||||||
|
this.speedBytesPerPackage.clear();
|
||||||
this.statsCache = null;
|
this.statsCache = null;
|
||||||
this.statsCacheAt = 0;
|
this.statsCacheAt = 0;
|
||||||
}
|
}
|
||||||
@ -1136,6 +1140,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.speedEvents = [];
|
this.speedEvents = [];
|
||||||
this.speedEventsHead = 0;
|
this.speedEventsHead = 0;
|
||||||
this.speedBytesLastWindow = 0;
|
this.speedBytesLastWindow = 0;
|
||||||
|
this.speedBytesPerPackage.clear();
|
||||||
this.packagePostProcessTasks.clear();
|
this.packagePostProcessTasks.clear();
|
||||||
this.packagePostProcessAbortControllers.clear();
|
this.packagePostProcessAbortControllers.clear();
|
||||||
this.hybridExtractRequeue.clear();
|
this.hybridExtractRequeue.clear();
|
||||||
@ -2324,6 +2329,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.session.reconnectReason = "";
|
this.session.reconnectReason = "";
|
||||||
this.speedEvents = [];
|
this.speedEvents = [];
|
||||||
this.speedBytesLastWindow = 0;
|
this.speedBytesLastWindow = 0;
|
||||||
|
this.speedBytesPerPackage.clear();
|
||||||
this.speedEventsHead = 0;
|
this.speedEventsHead = 0;
|
||||||
this.lastGlobalProgressBytes = 0;
|
this.lastGlobalProgressBytes = 0;
|
||||||
this.lastGlobalProgressAt = nowMs();
|
this.lastGlobalProgressAt = nowMs();
|
||||||
@ -2352,6 +2358,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.consecutiveReconnects = 0;
|
this.consecutiveReconnects = 0;
|
||||||
this.speedEvents = [];
|
this.speedEvents = [];
|
||||||
this.speedBytesLastWindow = 0;
|
this.speedBytesLastWindow = 0;
|
||||||
|
this.speedBytesPerPackage.clear();
|
||||||
this.speedEventsHead = 0;
|
this.speedEventsHead = 0;
|
||||||
this.lastGlobalProgressBytes = 0;
|
this.lastGlobalProgressBytes = 0;
|
||||||
this.lastGlobalProgressAt = nowMs();
|
this.lastGlobalProgressAt = nowMs();
|
||||||
@ -2443,6 +2450,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
this.speedEvents = [];
|
this.speedEvents = [];
|
||||||
this.speedBytesLastWindow = 0;
|
this.speedBytesLastWindow = 0;
|
||||||
|
this.speedBytesPerPackage.clear();
|
||||||
this.speedEventsHead = 0;
|
this.speedEventsHead = 0;
|
||||||
this.runItemIds.clear();
|
this.runItemIds.clear();
|
||||||
this.runPackageIds.clear();
|
this.runPackageIds.clear();
|
||||||
@ -2658,11 +2666,16 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private speedEventsHead = 0;
|
private speedEventsHead = 0;
|
||||||
|
private speedBytesPerPackage = new Map<string, number>();
|
||||||
|
|
||||||
private pruneSpeedEvents(now: number): void {
|
private pruneSpeedEvents(now: number): void {
|
||||||
const cutoff = now - 3000;
|
const cutoff = now - 3000;
|
||||||
while (this.speedEventsHead < this.speedEvents.length && this.speedEvents[this.speedEventsHead].at < cutoff) {
|
while (this.speedEventsHead < this.speedEvents.length && this.speedEvents[this.speedEventsHead].at < cutoff) {
|
||||||
this.speedBytesLastWindow = Math.max(0, this.speedBytesLastWindow - this.speedEvents[this.speedEventsHead].bytes);
|
const ev = this.speedEvents[this.speedEventsHead];
|
||||||
|
this.speedBytesLastWindow = Math.max(0, this.speedBytesLastWindow - ev.bytes);
|
||||||
|
const pkgBytes = (this.speedBytesPerPackage.get(ev.pid) ?? 0) - ev.bytes;
|
||||||
|
if (pkgBytes <= 0) this.speedBytesPerPackage.delete(ev.pid);
|
||||||
|
else this.speedBytesPerPackage.set(ev.pid, pkgBytes);
|
||||||
this.speedEventsHead += 1;
|
this.speedEventsHead += 1;
|
||||||
}
|
}
|
||||||
if (this.speedEventsHead > 200) {
|
if (this.speedEventsHead > 200) {
|
||||||
@ -2673,19 +2686,20 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private lastSpeedPruneAt = 0;
|
private lastSpeedPruneAt = 0;
|
||||||
|
|
||||||
private recordSpeed(bytes: number): void {
|
private recordSpeed(bytes: number, packageId: string = ""): void {
|
||||||
const now = nowMs();
|
const now = nowMs();
|
||||||
if (bytes > 0 && this.consecutiveReconnects > 0) {
|
if (bytes > 0 && this.consecutiveReconnects > 0) {
|
||||||
this.consecutiveReconnects = 0;
|
this.consecutiveReconnects = 0;
|
||||||
}
|
}
|
||||||
const bucket = now - (now % 120);
|
const bucket = now - (now % 120);
|
||||||
const last = this.speedEvents[this.speedEvents.length - 1];
|
const last = this.speedEvents[this.speedEvents.length - 1];
|
||||||
if (last && last.at === bucket) {
|
if (last && last.at === bucket && last.pid === packageId) {
|
||||||
last.bytes += bytes;
|
last.bytes += bytes;
|
||||||
} else {
|
} else {
|
||||||
this.speedEvents.push({ at: bucket, bytes });
|
this.speedEvents.push({ at: bucket, bytes, pid: packageId });
|
||||||
}
|
}
|
||||||
this.speedBytesLastWindow += bytes;
|
this.speedBytesLastWindow += bytes;
|
||||||
|
this.speedBytesPerPackage.set(packageId, (this.speedBytesPerPackage.get(packageId) ?? 0) + bytes);
|
||||||
if (now - this.lastSpeedPruneAt >= 1500) {
|
if (now - this.lastSpeedPruneAt >= 1500) {
|
||||||
this.pruneSpeedEvents(now);
|
this.pruneSpeedEvents(now);
|
||||||
this.lastSpeedPruneAt = now;
|
this.lastSpeedPruneAt = now;
|
||||||
@ -4251,7 +4265,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
windowBytes += buffer.length;
|
windowBytes += buffer.length;
|
||||||
this.session.totalDownloadedBytes += buffer.length;
|
this.session.totalDownloadedBytes += buffer.length;
|
||||||
this.itemContributedBytes.set(active.itemId, (this.itemContributedBytes.get(active.itemId) || 0) + buffer.length);
|
this.itemContributedBytes.set(active.itemId, (this.itemContributedBytes.get(active.itemId) || 0) + buffer.length);
|
||||||
this.recordSpeed(buffer.length);
|
this.recordSpeed(buffer.length, item.packageId);
|
||||||
throughputWindowBytes += buffer.length;
|
throughputWindowBytes += buffer.length;
|
||||||
|
|
||||||
const throughputNow = nowMs();
|
const throughputNow = nowMs();
|
||||||
@ -5380,6 +5394,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.speedEvents = [];
|
this.speedEvents = [];
|
||||||
this.speedEventsHead = 0;
|
this.speedEventsHead = 0;
|
||||||
this.speedBytesLastWindow = 0;
|
this.speedBytesLastWindow = 0;
|
||||||
|
this.speedBytesPerPackage.clear();
|
||||||
this.globalSpeedLimitQueue = Promise.resolve();
|
this.globalSpeedLimitQueue = Promise.resolve();
|
||||||
this.globalSpeedLimitNextAt = 0;
|
this.globalSpeedLimitNextAt = 0;
|
||||||
this.nonResumableActive = 0;
|
this.nonResumableActive = 0;
|
||||||
|
|||||||
@ -77,7 +77,7 @@ const emptySnapshot = (): UiSnapshot => ({
|
|||||||
paused: false, running: false, updatedAt: Date.now()
|
paused: false, running: false, updatedAt: Date.now()
|
||||||
},
|
},
|
||||||
summary: null, stats: emptyStats(), speedText: "Geschwindigkeit: 0 B/s", etaText: "ETA: --",
|
summary: null, stats: emptyStats(), speedText: "Geschwindigkeit: 0 B/s", etaText: "ETA: --",
|
||||||
canStart: true, canStop: false, canPause: false, clipboardActive: false, reconnectSeconds: 0
|
canStart: true, canStop: false, canPause: false, clipboardActive: false, reconnectSeconds: 0, packageSpeedBps: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
const cleanupLabels: Record<string, string> = {
|
const cleanupLabels: Record<string, string> = {
|
||||||
@ -1704,13 +1704,11 @@ export function App(): ReactElement {
|
|||||||
|
|
||||||
const packageSpeedMap = useMemo(() => {
|
const packageSpeedMap = useMemo(() => {
|
||||||
const map = new Map<string, number>();
|
const map = new Map<string, number>();
|
||||||
for (const item of Object.values(snapshot.session.items)) {
|
for (const [pid, bps] of Object.entries(snapshot.packageSpeedBps)) {
|
||||||
if (item.speedBps > 0) {
|
if (bps > 0) map.set(pid, bps);
|
||||||
map.set(item.packageId, (map.get(item.packageId) ?? 0) + item.speedBps);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [snapshot.session.items]);
|
}, [snapshot.packageSpeedBps]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -2527,18 +2525,27 @@ export function App(): ReactElement {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!multi && contextMenu.itemId && (
|
{!multi && contextMenu.itemId && (
|
||||||
<button className="ctx-menu-item ctx-danger" onClick={() => { void window.rd.removeItem(contextMenu.itemId!); setContextMenu(null); }}>Entfernen</button>
|
<button className="ctx-menu-item ctx-danger" onClick={() => {
|
||||||
|
setContextMenu(null);
|
||||||
|
const ids = new Set([contextMenu.itemId!]);
|
||||||
|
if (settingsDraft.confirmDeleteSelection) { setDeleteConfirm({ ids, dontAsk: false }); }
|
||||||
|
else { executeDeleteSelection(ids); }
|
||||||
|
}}>Entfernen</button>
|
||||||
)}
|
)}
|
||||||
{multi && hasItems && (
|
{multi && hasItems && (
|
||||||
<button className="ctx-menu-item ctx-danger" onClick={() => {
|
<button className="ctx-menu-item ctx-danger" onClick={() => {
|
||||||
for (const id of selectedIds) { if (snapshot.session.items[id]) void window.rd.removeItem(id); }
|
setContextMenu(null);
|
||||||
setSelectedIds(new Set()); setContextMenu(null);
|
const ids = new Set([...selectedIds].filter((id) => snapshot.session.items[id]));
|
||||||
|
if (settingsDraft.confirmDeleteSelection) { setDeleteConfirm({ ids, dontAsk: false }); }
|
||||||
|
else { executeDeleteSelection(ids); }
|
||||||
}}>Ausgewählte entfernen ({[...selectedIds].filter((id) => snapshot.session.items[id]).length})</button>
|
}}>Ausgewählte entfernen ({[...selectedIds].filter((id) => snapshot.session.items[id]).length})</button>
|
||||||
)}
|
)}
|
||||||
{hasPackages && (
|
{hasPackages && (
|
||||||
<button className="ctx-menu-item ctx-danger" onClick={() => {
|
<button className="ctx-menu-item ctx-danger" onClick={() => {
|
||||||
for (const id of selectedIds) { if (snapshot.session.packages[id]) onPackageCancel(id); }
|
setContextMenu(null);
|
||||||
setSelectedIds(new Set()); setContextMenu(null);
|
const ids = new Set([...selectedIds].filter((id) => snapshot.session.packages[id]));
|
||||||
|
if (settingsDraft.confirmDeleteSelection) { setDeleteConfirm({ ids, dontAsk: false }); }
|
||||||
|
else { executeDeleteSelection(ids); }
|
||||||
}}>{multi ? `Ausgewählte löschen (${[...selectedIds].filter((id) => snapshot.session.packages[id]).length})` : "Löschen"}</button>
|
}}>{multi ? `Ausgewählte löschen (${[...selectedIds].filter((id) => snapshot.session.packages[id]).length})` : "Löschen"}</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -2683,10 +2690,10 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
|
|||||||
<span className="pkg-col pkg-col-speed">{packageSpeed > 0 ? formatSpeedMbps(packageSpeed) : "-"}</span>
|
<span className="pkg-col pkg-col-speed">{packageSpeed > 0 ? formatSpeedMbps(packageSpeed) : "-"}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{!collapsed && <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}%` }} />}
|
{extracting && <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" : ""}`} 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" : ""}`} 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); }}>
|
||||||
<span className="pkg-col pkg-col-name item-indent" title={item.fileName}>{item.fileName}</span>
|
<span className="pkg-col pkg-col-name item-indent" title={item.fileName}>{item.fileName}</span>
|
||||||
|
|||||||
@ -161,6 +161,7 @@ export interface UiSnapshot {
|
|||||||
canPause: boolean;
|
canPause: boolean;
|
||||||
clipboardActive: boolean;
|
clipboardActive: boolean;
|
||||||
reconnectSeconds: number;
|
reconnectSeconds: number;
|
||||||
|
packageSpeedBps: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddLinksPayload {
|
export interface AddLinksPayload {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user