From 8fb92caa30ab2f713e298ea1c7c82b0fedba02e0 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 21 Aug 2026 01:06:03 +0200 Subject: [PATCH] feat(downloads): explain unknown remaining sizes --- src/renderer/App.tsx | 3 ++- src/renderer/i18n.ts | 4 ++++ src/renderer/views/downloads/DownloadsView.tsx | 10 +++++++--- .../views/downloads/downloads-model.ts | 5 +++++ src/renderer/views/downloads/downloads.css | 15 +++++++++++++++ tests/downloads-view.test.tsx | 18 ++++++++++++++++++ tests/i18n.test.ts | 2 ++ 7 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 1442915..f080da2 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -95,7 +95,7 @@ import { StatisticsSidebarStatus, type StatisticsViewActions } from "./views/statistics/StatisticsView"; -import { buildDownloadsViewModel, formatRemainingDownloadBytes, getDownloadQueueTotalBytes, getDownloadSpeedBps, getPendingDownloadItemCount, getRemainingDownloadBytes, type DownloadDisplayMode, type DownloadSidebarFilter } from "./views/downloads/downloads-model"; +import { buildDownloadsViewModel, formatRemainingDownloadBytes, formatRemainingDownloadTooltip, getDownloadQueueTotalBytes, getDownloadSpeedBps, getPendingDownloadItemCount, getRemainingDownloadBytes, type DownloadDisplayMode, type DownloadSidebarFilter } from "./views/downloads/downloads-model"; import { downloadColumnDefinitions, type DownloadSortColumn } from "./views/downloads/DownloadsTable"; import { DeleteConfirmationDialog } from "./views/downloads/DeleteConfirmationDialog"; import { beginDownloadColumnDrag, clearDownloadColumnDrag, commitDownloadColumnDrag, createDownloadColumnOrderPersistence, DOWNLOAD_COLUMN_MOVE_DURATION_MS, updateDownloadColumnDrag, type DownloadColumnDragSession, type DownloadColumnOrderPersistence } from "./views/downloads/column-drag"; @@ -4826,6 +4826,7 @@ export function App(): ReactElement { totalBytes: downloadQueueTotalBytes, remaining: formatRemainingDownloadBytes(downloadRemaining), remainingBytes: downloadRemaining.bytes, + remainingTooltip: formatRemainingDownloadTooltip(downloadRemaining), hosters: providerStats.length, speed: liveDownloadSpeedBps > 0 ? formatSpeedMbps(liveDownloadSpeedBps) : "0 B/s", eta: snapshot.etaText diff --git a/src/renderer/i18n.ts b/src/renderer/i18n.ts index 594df38..d07aff7 100644 --- a/src/renderer/i18n.ts +++ b/src/renderer/i18n.ts @@ -251,6 +251,8 @@ function translateDynamic(value: string, language: AppLanguage): string { } const remaining = value.match(/^(.+) von (.+) übrig$/); if (remaining) return `${remaining[1]} of ${remaining[2]} remaining`; + const unknownRemaining = value.match(/^Noch unbekannte Dateigrößen: (\d+)\. Die tatsächliche Restmenge kann höher sein\.$/); + if (unknownRemaining) return `Unknown file sizes: ${unknownRemaining[1]}. The actual remaining amount may be higher.`; const actionsFor = value.match(/^Aktionen für (.+)$/); if (actionsFor) return `Actions for ${actionsFor[1]}`; const assignment = value.match(/^(.+) Zuordnung entfernen$/); @@ -416,6 +418,8 @@ function translateDynamic(value: string, language: AppLanguage): string { } const remaining = value.match(/^(.+) of (.+) remaining$/); if (remaining) return `${remaining[1]} von ${remaining[2]} übrig`; + const unknownRemaining = value.match(/^Unknown file sizes: (\d+)\. The actual remaining amount may be higher\.$/); + if (unknownRemaining) return `Noch unbekannte Dateigrößen: ${unknownRemaining[1]}. Die tatsächliche Restmenge kann höher sein.`; const actionsFor = value.match(/^Actions for (.+)$/); if (actionsFor) return `Aktionen für ${actionsFor[1]}`; const assignment = value.match(/^Remove (.+) assignment$/); diff --git a/src/renderer/views/downloads/DownloadsView.tsx b/src/renderer/views/downloads/DownloadsView.tsx index eee5eff..c573afa 100644 --- a/src/renderer/views/downloads/DownloadsView.tsx +++ b/src/renderer/views/downloads/DownloadsView.tsx @@ -21,6 +21,7 @@ export interface DownloadsStatusModel { totalBytes: number; remaining: string; remainingBytes: number; + remainingTooltip?: string; hosters: number; speed: string; eta: string; @@ -106,15 +107,18 @@ export function DownloadsSidebar({ actions, model }: { actions: DownloadsViewAct export function DownloadsSidebarStatus({ model }: { model: DownloadsViewModel }): ReactElement { const speed = model.status.speed.replace(/^Geschwindigkeit:\s*/i, ""); const eta = model.status.eta.replace(/^ETA:\s*/i, ""); - const entries = [ + const entries: Array<{ label: string; metric: string; numericValue: number; value: string; tooltip?: string }> = [ { label: "Pakete", metric: "packages", numericValue: model.status.packages, value: integerFormatter.format(model.status.packages) }, { label: "Links", metric: "links", numericValue: model.status.links, value: integerFormatter.format(model.status.links) }, { label: "Sitzung", metric: "session", numericValue: model.status.sessionBytes, value: model.status.session }, - { label: "Verbleibend", metric: "remaining", numericValue: model.status.remainingBytes, value: model.status.remaining }, + { label: "Verbleibend", metric: "remaining", numericValue: model.status.remainingBytes, value: model.status.remaining, tooltip: model.status.remainingTooltip }, { label: "Gesamt", metric: "total", numericValue: model.status.totalBytes, value: model.status.total }, { label: "Hoster", metric: "hosters", numericValue: model.status.hosters, value: integerFormatter.format(model.status.hosters) } ]; - return
{entries.map((entry) =>
{entry.label}
)}
Geschwindigkeit{speed}
ETA{eta}
; + return
{entries.map((entry) => { + const tooltipId = entry.tooltip ? `downloads-status-${entry.metric}-tooltip` : undefined; + return
{entry.label}{entry.tooltip ? {entry.tooltip} : null}
; + })}
Geschwindigkeit{speed}
ETA{eta}
; } export function DownloadsToolbar({ actions, model }: { actions: DownloadsViewActions; model: DownloadsViewModel }): ReactElement { diff --git a/src/renderer/views/downloads/downloads-model.ts b/src/renderer/views/downloads/downloads-model.ts index 79ce77a..0b23678 100644 --- a/src/renderer/views/downloads/downloads-model.ts +++ b/src/renderer/views/downloads/downloads-model.ts @@ -121,6 +121,11 @@ export function formatRemainingDownloadBytes(summary: { bytes: number; unknownIt return summary.bytes > 0 ? `≥ ${humanSize(summary.bytes)}` : "Unbekannt"; } +export function formatRemainingDownloadTooltip(summary: { bytes: number; unknownItems: number }): string { + if (summary.unknownItems <= 0) return ""; + return `Noch unbekannte Dateigrößen: ${summary.unknownItems}. Die tatsächliche Restmenge kann höher sein.`; +} + export function getDownloadSpeedBps(packageSpeeds: Record): number { let total = 0; for (const speed of Object.values(packageSpeeds)) { diff --git a/src/renderer/views/downloads/downloads.css b/src/renderer/views/downloads/downloads.css index 4c70296..e5eee17 100644 --- a/src/renderer/views/downloads/downloads.css +++ b/src/renderer/views/downloads/downloads.css @@ -155,6 +155,21 @@ font-size: 12px; } +.downloads-sidebar-status .has-tooltip { + cursor: help; +} + +.downloads-visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + border: 0; + white-space: nowrap; +} + .downloads-sidebar-status strong { color: var(--ui-text); font-weight: 600; diff --git a/tests/downloads-view.test.tsx b/tests/downloads-view.test.tsx index 9e4fd8c..e525581 100644 --- a/tests/downloads-view.test.tsx +++ b/tests/downloads-view.test.tsx @@ -10,6 +10,7 @@ import { buildDownloadsViewModel, classifyDownloadStatus, formatRemainingDownloadBytes, + formatRemainingDownloadTooltip, getDownloadQueueTotalBytes, getRemainingDownloadBytes, getPendingDownloadItemCount, @@ -248,6 +249,22 @@ describe("verbleibendes Downloadvolumen", () => { expect(formatRemainingDownloadBytes({ bytes: 750_000_000, unknownItems: 1 })).toBe("≥ 715.26 MB"); expect(formatRemainingDownloadBytes({ bytes: 0, unknownItems: 1 })).toBe("Unbekannt"); expect(formatRemainingDownloadBytes({ bytes: 0, unknownItems: 0 })).toBe("0 B"); + expect(formatRemainingDownloadTooltip({ bytes: 750_000_000, unknownItems: 1 })) + .toBe("Noch unbekannte Dateigrößen: 1. Die tatsächliche Restmenge kann höher sein."); + expect(formatRemainingDownloadTooltip({ bytes: 0, unknownItems: 0 })).toBe(""); + }); + + it("erklärt unbekannte Restgrößen direkt am verbleibenden Wert", () => { + const model = withRuntime(createInput()); + model.status.remaining = "≥ 715.26 MB"; + model.status.remainingTooltip = "Noch unbekannte Dateigrößen: 2. Die tatsächliche Restmenge kann höher sein."; + + const html = renderToStaticMarkup(); + + expect(html).toContain('class="has-tooltip"'); + expect(html).toContain('title="Noch unbekannte Dateigrößen: 2. Die tatsächliche Restmenge kann höher sein."'); + expect(html).toContain('aria-describedby="downloads-status-remaining-tooltip"'); + expect(html).toContain('class="downloads-visually-hidden"'); }); }); @@ -766,6 +783,7 @@ function withRuntime(input: DownloadsModelInput, overrides: Record { expect(translateUiText("Klicken zum Kopieren", "en")).toBe("Click to copy"); expect(translateUiText("Geprüft", "en")).toBe("Checked"); expect(translateUiText("gerade eben", "en")).toBe("just now"); + expect(translateUiText("Noch unbekannte Dateigrößen: 3. Die tatsächliche Restmenge kann höher sein.", "en")) + .toBe("Unknown file sizes: 3. The actual remaining amount may be higher."); expect(translateUiText("Geschwindigkeit verschieben", "en")).toBe("Move Speed"); expect(translateUiText("Geschwindigkeit nach links verschieben", "en")).toBe("Move Speed left"); expect(translateUiText("Move Speed right", "de")).toBe("Geschwindigkeit nach rechts verschieben");