feat(downloads): explain unknown remaining sizes

This commit is contained in:
Sucukdeluxe
2026-08-21 01:06:03 +02:00
parent 869dbc25be
commit 8fb92caa30
7 changed files with 53 additions and 4 deletions
+2 -1
View File
@@ -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
+4
View File
@@ -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$/);
@@ -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 <section className="downloads-sidebar-status" data-visual-region="downloads-sidebar-status" aria-label="Downloadstatus">{entries.map((entry) => <div key={entry.metric}><span>{entry.label}</span><RollingMetricValue numericValue={entry.numericValue} value={entry.value} /></div>)}<div><span>Geschwindigkeit</span><strong data-status-metric="speed">{speed}</strong></div><div><span>ETA</span><strong data-status-metric="eta">{eta}</strong></div></section>;
return <section className="downloads-sidebar-status" data-visual-region="downloads-sidebar-status" aria-label="Downloadstatus">{entries.map((entry) => {
const tooltipId = entry.tooltip ? `downloads-status-${entry.metric}-tooltip` : undefined;
return <div aria-describedby={tooltipId} className={entry.tooltip ? "has-tooltip" : undefined} key={entry.metric} title={entry.tooltip || undefined}><span>{entry.label}</span><RollingMetricValue numericValue={entry.numericValue} value={entry.value} />{entry.tooltip ? <span className="downloads-visually-hidden" id={tooltipId}>{entry.tooltip}</span> : null}</div>;
})}<div><span>Geschwindigkeit</span><strong data-status-metric="speed">{speed}</strong></div><div><span>ETA</span><strong data-status-metric="eta">{eta}</strong></div></section>;
}
export function DownloadsToolbar({ actions, model }: { actions: DownloadsViewActions; model: DownloadsViewModel }): ReactElement {
@@ -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<string, number>): number {
let total = 0;
for (const speed of Object.values(packageSpeeds)) {
@@ -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;
+18
View File
@@ -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(<DownloadsSidebarStatus model={model} />);
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<string, unkno
totalBytes: 10_000_000_000,
remaining: "6,50 GB",
remainingBytes: 6_500_000_000,
remainingTooltip: "",
hosters: 3,
speed: "96,00 Mbit/s",
eta: "00:05:00"
+2
View File
@@ -75,6 +75,8 @@ describe("renderer localization", () => {
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");