From b670b92147c43807b64c7f3cdbef2b74450f36d2 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Mon, 10 Aug 2026 20:38:11 +0200 Subject: [PATCH] release: publish Multi-Debrid Downloader v2.0.15 Synchronize live speed telemetry, preserve known package sizes, refine responsive download status presentation, improve progress contrast and accessibility, simplify account creation, and prepare the verified v2.0.15 desktop release. --- CHANGELOG.md | 36 +++++ package-lock.json | 4 +- package.json | 2 +- src/main/download-manager.ts | 7 +- src/main/download-size.ts | 12 ++ src/renderer/App.tsx | 67 ++++----- src/renderer/download-format.ts | 4 + src/renderer/shell/shell.css | 2 +- src/renderer/theme.css | 6 + .../views/downloads/DownloadsTable.tsx | 71 +++++++--- .../views/downloads/DownloadsView.tsx | 2 - .../views/downloads/downloads-model.ts | 8 ++ src/renderer/views/downloads/downloads.css | 43 +++++- .../views/settings/AccountWorkspace.tsx | 70 ++++------ src/renderer/views/settings/settings.css | 50 ++----- tests/app-shell.test.tsx | 14 +- tests/download-size.test.ts | 22 +++ tests/downloads-view.test.tsx | 129 +++++++++++++++++- tests/settings-view.test.tsx | 49 ++++++- tests/statistics-view.test.tsx | 26 +++- 20 files changed, 452 insertions(+), 172 deletions(-) create mode 100644 src/main/download-size.ts create mode 100644 tests/download-size.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index db15f94..93da029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ All notable changes to Multi-Debrid Downloader are documented in this file. +## [2.0.15] - 2026-08-10 + +### Highlights + +- Unified live speed reporting across the header, package table, and sidebar with a single telemetry source and consistent two-decimal formatting. +- Reworked account creation into one clear service and access-type selector with the relevant credentials form directly below it. +- Improved dense download views with responsive status labels, clearer progress text, and safer package expansion behavior. + +### Downloads and telemetry + +- Removed the redundant Add links toolbar action so Start is now the first download control. +- Removed the additional renderer delay for large active queues, allowing manager telemetry to appear without a second buffering interval. +- Kept the header sparkline, package speeds, and sidebar speed synchronized from the same package telemetry snapshot. +- Preserved known file sizes when an unrestrict response does not provide a replacement size, preventing temporary queue-total drops when a download starts. +- Restricted package expansion and collapse to the visible disclosure button so ordinary row clicks no longer change the package state. +- Added compact window labels for link conversion, active downloads, and extraction while retaining complete status details in tooltips and accessibility labels. +- Removed duplicated access-mode wording from Mega-Debrid service labels while preserving the complete source label as a tooltip. + +### Interface and accessibility + +- Added clipped dual-color progress labels so text remains light over the unfilled track and dark over the green fill. +- Added a dedicated orange bandwidth-chart accent with a restrained matching area fill. +- Changed active premium account indicators to the success color. +- Kept the File, Settings, and Help menus inside the application frame at narrow window widths. +- Added semantic progressbar values and accessible labels to package and file size/progress meters. + +### Settings and accounts + +- Replaced the expandable account-type list with a single service/access selector. +- Displayed the selected account type description and exactly one matching credentials form. +- Retained the existing account validation, protected secret fields, and save flow. + +### Reliability and testing + +- Added regression coverage for stable file-size transitions, unified speed telemetry, responsive status presentation, service-label cleanup, progress contrast, package disclosure behavior, account selection, premium status colors, chart colors, toolbar ordering, and narrow application menus. + ## [2.0.14] - 2026-08-10 ### Highlights diff --git a/package-lock.json b/package-lock.json index 43ce184..bc006b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "2.0.14", + "version": "2.0.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "2.0.14", + "version": "2.0.15", "license": "MIT", "dependencies": { "adm-zip": "0.6.0", diff --git a/package.json b/package.json index 5109667..e779cfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "2.0.14", + "version": "2.0.15", "description": "Desktop downloader", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 03bea0b..456980d 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -67,7 +67,8 @@ import { ensurePackageLog, getPackageLogPath as getPersistedPackageLogPath, logP import { logRenameEvent as writeRenameLogEvent } from "./rename-log"; import { logDesktopRename, verifyRename, verifyRenameAsync, type RenameVerification } from "./desktop-rename-log"; import { StoragePaths, saveSession, saveSessionAsync, saveSettings, saveSettingsAsync } from "./storage"; -import { compactErrorText, ensureDirPath, filenameFromUrl, formatEta, humanSize, looksLikeOpaqueFilename, nowMs, sanitizeFilename, sleep } from "./utils"; +import { compactErrorText, ensureDirPath, filenameFromUrl, formatEta, humanSize, looksLikeOpaqueFilename, nowMs, sanitizeFilename, sleep } from "./utils"; +import { mergeKnownTotalBytes } from "./download-size"; type ActiveTask = { itemId: string; @@ -9044,7 +9045,7 @@ export class DownloadManager extends EventEmitter { ? existingTargetPath : path.join(pkg.outputDir, item.fileName); item.targetPath = this.claimTargetPath(item.id, preferredTargetPath, Boolean(canReuseExistingTarget)); - item.totalBytes = unrestricted.fileSize; + item.totalBytes = mergeKnownTotalBytes(item.totalBytes, unrestricted.fileSize); item.status = "downloading"; const pLabel = unrestricted.providerLabel; const statusLabel = providerLabel(unrestricted.provider) || pLabel; @@ -9122,7 +9123,7 @@ export class DownloadManager extends EventEmitter { item.status = "integrity_check"; item.progressPercent = 0; item.downloadedBytes = 0; - item.totalBytes = unrestricted.fileSize; + item.totalBytes = mergeKnownTotalBytes(item.totalBytes, unrestricted.fileSize); this.emitState(); await sleep(300); continue; diff --git a/src/main/download-size.ts b/src/main/download-size.ts new file mode 100644 index 0000000..e968ec3 --- /dev/null +++ b/src/main/download-size.ts @@ -0,0 +1,12 @@ +export function mergeKnownTotalBytes( + currentTotalBytes: number | null | undefined, + replacementTotalBytes: number | null | undefined +): number | null { + if (replacementTotalBytes != null && Number.isFinite(replacementTotalBytes) && replacementTotalBytes > 0) { + return replacementTotalBytes; + } + if (currentTotalBytes != null && Number.isFinite(currentTotalBytes) && currentTotalBytes > 0) { + return currentTotalBytes; + } + return null; +} diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 4494b7c..3caf19a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -90,7 +90,7 @@ import { StatisticsSidebarStatus, type StatisticsViewActions } from "./views/statistics/StatisticsView"; -import { buildDownloadsViewModel, getDownloadQueueTotalBytes, type DownloadDisplayMode, type DownloadSidebarFilter } from "./views/downloads/downloads-model"; +import { buildDownloadsViewModel, getDownloadQueueTotalBytes, getDownloadSpeedBps, type DownloadDisplayMode, type DownloadSidebarFilter } from "./views/downloads/downloads-model"; import { downloadColumnDefinitions, type DownloadSortColumn } from "./views/downloads/DownloadsTable"; import { beginDownloadColumnDrag, clearDownloadColumnDrag, settleDownloadColumnDrag, updateDownloadColumnDrag, type DownloadColumnDragSession } from "./views/downloads/column-drag"; import { @@ -1082,7 +1082,14 @@ const historyRetentionLabels: Record= 700 ? 0 : itemCount >= 250 ? 50 : 100; + if (!running) delay = Math.min(delay, 200); + if (activeTab !== "downloads") delay = Math.max(delay, 800); + return delay; +} const KNOWN_HOSTERS: { id: string; label: string }[] = [ { id: "rapidgator", label: "Rapidgator" }, @@ -1304,7 +1311,7 @@ export function readBandwidthChartPalette( return { grid: readProperty("--ui-border").trim(), text: readProperty("--ui-text-muted").trim(), - accent: readProperty("--ui-accent").trim(), + accent: readProperty("--ui-speed-accent").trim(), fontFamily: fontFamily.trim() }; } @@ -1507,20 +1514,15 @@ const BandwidthChart = memo(function BandwidthChart({ items, running, paused, sp }); interface DownloadSpeedSparklineProps { - items: Record; - running: boolean; - paused: boolean; + speedBps: number; speedStateRef: React.MutableRefObject; hidden?: boolean; } -const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, running, paused, speedStateRef, hidden = false }: DownloadSpeedSparklineProps): ReactElement { +const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ speedBps, speedStateRef, hidden = false }: DownloadSpeedSparklineProps): ReactElement { const canvasRef = useRef(null); - const itemsRef = useRef(items); - const activeRef = useRef(running && !paused); - const [liveSpeed, setLiveSpeed] = useState(0); - itemsRef.current = items; - activeRef.current = running && !paused; + const speedRef = useRef(speedBps); + speedRef.current = speedBps; useEffect(() => { const draw = (): void => { @@ -1571,17 +1573,11 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run ctx.lineWidth = 1.5; ctx.lineJoin = "round"; ctx.stroke(); - }; - - const tick = (): void => { - let target = 0; - if (activeRef.current) { - for (const it of Object.values(itemsRef.current)) { - if (it.status === "downloading") target += it.speedBps || 0; - } - } + }; + + const tick = (): void => { + const target = speedRef.current; speedStateRef.current = updateDownloadSpeedHistory(speedStateRef.current, target); - setLiveSpeed(target); draw(); }; @@ -1592,7 +1588,7 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ items, run return (
- {liveSpeed > 0 ? formatSpeedMbps(liveSpeed) : "0 B/s"} + {speedBps > 0 ? formatSpeedMbps(speedBps) : "0 B/s"}
); }); @@ -2165,20 +2161,8 @@ export function App(): ReactElement { latestStateRef.current = merged; if (stateFlushTimerRef.current) { return; } - const itemCount = Object.keys(merged.session.items).length; - let flushDelay = itemCount >= 1500 - ? 900 - : itemCount >= 700 - ? 650 - : itemCount >= 250 - ? 400 - : 150; - if (!merged.session.running) { - flushDelay = Math.min(flushDelay, 200); - } - if (activeTabRef.current !== "downloads") { - flushDelay = Math.max(flushDelay, 800); - } + const itemCount = Object.keys(merged.session.items).length; + const flushDelay = getSnapshotRenderDelay(itemCount, merged.session.running, activeTabRef.current); stateFlushTimerRef.current = setTimeout(() => { stateFlushTimerRef.current = null; @@ -4961,6 +4945,7 @@ export function App(): ReactElement { }, [downloadsViewCore.actionableSelectedIds, executeDeleteSelection, settingsDraft.confirmDeleteSelection]); const downloadPackageSpeeds = useMemo(() => Object.fromEntries(packageSpeedMap), [packageSpeedMap]); + const liveDownloadSpeedBps = useMemo(() => getDownloadSpeedBps(snapshot.packageSpeedBps), [snapshot.packageSpeedBps]); const downloadQueueTotalBytes = useMemo(() => getDownloadQueueTotalBytes(Object.values(snapshot.session.items)), [snapshot.session.items]); const downloadsViewModel = useMemo(() => ({ ...downloadsViewCore, @@ -4992,10 +4977,10 @@ export function App(): ReactElement { total: humanSize(downloadQueueTotalBytes), totalBytes: downloadQueueTotalBytes, hosters: providerStats.length, - speed: snapshot.speedText, + speed: liveDownloadSpeedBps > 0 ? formatSpeedMbps(liveDownloadSpeedBps) : "0 B/s", eta: snapshot.etaText } - }), [actionBusy, columnOrder, downloadPackageSpeeds, downloadQueueTotalBytes, downloadsSortColumn, downloadsSortDescending, downloadsViewCore, editingName, editingPackageId, gridTemplate, providerStats.length, scheduleCountdown, schedulePickerOpen, scheduleTimeInput, snapshot.canPause, snapshot.canStart, snapshot.canStop, snapshot.clipboardActive, snapshot.etaText, snapshot.reconnectSeconds, snapshot.session.items, snapshot.session.paused, snapshot.session.reconnectReason, snapshot.session.running, snapshot.settings.scheduledStartEpochMs, snapshot.speedText, snapshot.stats.totalDownloaded, snapshot.stats.totalPackages]); + }), [actionBusy, columnOrder, downloadPackageSpeeds, downloadQueueTotalBytes, downloadsSortColumn, downloadsSortDescending, downloadsViewCore, editingName, editingPackageId, gridTemplate, liveDownloadSpeedBps, providerStats.length, scheduleCountdown, schedulePickerOpen, scheduleTimeInput, snapshot.canPause, snapshot.canStart, snapshot.canStop, snapshot.clipboardActive, snapshot.etaText, snapshot.reconnectSeconds, snapshot.session.items, snapshot.session.paused, snapshot.session.reconnectReason, snapshot.session.running, snapshot.settings.scheduledStartEpochMs, snapshot.stats.totalDownloaded, snapshot.stats.totalPackages]); const downloadsActions: DownloadsViewActions = { onDisplayModeChange: setDownloadDisplayMode, @@ -5651,9 +5636,7 @@ export function App(): ReactElement { headerActions={( <>