feat(downloads): explain unknown remaining sizes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user