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={( <>