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:
Sucukdeluxe 2026-03-02 18:45:40 +01:00
parent 0d1deadb6f
commit 670c2f1ff5
4 changed files with 44 additions and 21 deletions

View File

@ -1,6 +1,6 @@
{
"name": "real-debrid-downloader",
"version": "1.4.97",
"version": "1.4.98",
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
"main": "build/main/main/main.js",
"author": "Sucukdeluxe",

View File

@ -718,7 +718,7 @@ export class DownloadManager extends EventEmitter {
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;
@ -872,7 +872,10 @@ export class DownloadManager extends EventEmitter {
canStop: this.session.running,
canPause: this.session.running,
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.speedEventsHead = 0;
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.statsCache = null;
this.statsCacheAt = 0;
}
@ -1136,6 +1140,7 @@ export class DownloadManager extends EventEmitter {
this.speedEvents = [];
this.speedEventsHead = 0;
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.packagePostProcessTasks.clear();
this.packagePostProcessAbortControllers.clear();
this.hybridExtractRequeue.clear();
@ -2324,6 +2329,7 @@ export class DownloadManager extends EventEmitter {
this.session.reconnectReason = "";
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
this.lastGlobalProgressBytes = 0;
this.lastGlobalProgressAt = nowMs();
@ -2352,6 +2358,7 @@ export class DownloadManager extends EventEmitter {
this.consecutiveReconnects = 0;
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
this.lastGlobalProgressBytes = 0;
this.lastGlobalProgressAt = nowMs();
@ -2443,6 +2450,7 @@ export class DownloadManager extends EventEmitter {
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
this.runItemIds.clear();
this.runPackageIds.clear();
@ -2658,11 +2666,16 @@ export class DownloadManager extends EventEmitter {
}
private speedEventsHead = 0;
private speedBytesPerPackage = new Map<string, number>();
private pruneSpeedEvents(now: number): void {
const cutoff = now - 3000;
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;
}
if (this.speedEventsHead > 200) {
@ -2673,19 +2686,20 @@ export class DownloadManager extends EventEmitter {
private lastSpeedPruneAt = 0;
private recordSpeed(bytes: number): void {
private recordSpeed(bytes: number, packageId: string = ""): void {
const now = nowMs();
if (bytes > 0 && this.consecutiveReconnects > 0) {
this.consecutiveReconnects = 0;
}
const bucket = now - (now % 120);
const last = this.speedEvents[this.speedEvents.length - 1];
if (last && last.at === bucket) {
if (last && last.at === bucket && last.pid === packageId) {
last.bytes += bytes;
} else {
this.speedEvents.push({ at: bucket, bytes });
this.speedEvents.push({ at: bucket, bytes, pid: packageId });
}
this.speedBytesLastWindow += bytes;
this.speedBytesPerPackage.set(packageId, (this.speedBytesPerPackage.get(packageId) ?? 0) + bytes);
if (now - this.lastSpeedPruneAt >= 1500) {
this.pruneSpeedEvents(now);
this.lastSpeedPruneAt = now;
@ -4251,7 +4265,7 @@ export class DownloadManager extends EventEmitter {
windowBytes += buffer.length;
this.session.totalDownloadedBytes += 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;
const throughputNow = nowMs();
@ -5380,6 +5394,7 @@ export class DownloadManager extends EventEmitter {
this.speedEvents = [];
this.speedEventsHead = 0;
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.globalSpeedLimitQueue = Promise.resolve();
this.globalSpeedLimitNextAt = 0;
this.nonResumableActive = 0;

View File

@ -77,7 +77,7 @@ const emptySnapshot = (): UiSnapshot => ({
paused: false, running: false, updatedAt: Date.now()
},
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> = {
@ -1704,13 +1704,11 @@ export function App(): ReactElement {
const packageSpeedMap = useMemo(() => {
const map = new Map<string, number>();
for (const item of Object.values(snapshot.session.items)) {
if (item.speedBps > 0) {
map.set(item.packageId, (map.get(item.packageId) ?? 0) + item.speedBps);
}
for (const [pid, bps] of Object.entries(snapshot.packageSpeedBps)) {
if (bps > 0) map.set(pid, bps);
}
return map;
}, [snapshot.session.items]);
}, [snapshot.packageSpeedBps]);
return (
<div
@ -2527,18 +2525,27 @@ export function App(): ReactElement {
</button>
)}
{!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 && (
<button className="ctx-menu-item ctx-danger" onClick={() => {
for (const id of selectedIds) { if (snapshot.session.items[id]) void window.rd.removeItem(id); }
setSelectedIds(new Set()); setContextMenu(null);
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>
)}
{hasPackages && (
<button className="ctx-menu-item ctx-danger" onClick={() => {
for (const id of selectedIds) { if (snapshot.session.packages[id]) onPackageCancel(id); }
setSelectedIds(new Set()); setContextMenu(null);
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>
)}
</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>
</div>
</header>
{!collapsed && <div className="progress">
<div className="progress">
<div className="progress-dl" style={{ width: `${dlProgress}%` }} />
{extracting && <div className="progress-ex" style={{ width: `${exProgress}%` }} />}
</div>}
</div>
{!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); }}>
<span className="pkg-col pkg-col-name item-indent" title={item.fileName}>{item.fileName}</span>

View File

@ -161,6 +161,7 @@ export interface UiSnapshot {
canPause: boolean;
clipboardActive: boolean;
reconnectSeconds: number;
packageSpeedBps: Record<string, number>;
}
export interface AddLinksPayload {