fix(downloads): compact finalization statuses
Normalize German and English finalization producer labels before they reach package and item status cells. Derive percentages from valid fractions, clamp them to the visible 0-100 range, and reduce malformed fractions or zero totals to the phase name. Preserve legacy percentage labels while preventing finalization lines ending in archive names from being classified as bare extraction archives. Cover current and legacy forms, both languages, package and item rendering, localization, and invalid numeric input.
This commit is contained in:
@@ -254,6 +254,8 @@ function translateDynamic(value: string, language: AppLanguage): string {
|
|||||||
if (result) return `${result[1]} completed${result[2].replace(/(\d+) Fehler/g, "$1 errors").replace(/(\d+) abgebrochen/g, "$1 cancelled")}`;
|
if (result) return `${result[1]} completed${result[2].replace(/(\d+) Fehler/g, "$1 errors").replace(/(\d+) abgebrochen/g, "$1 cancelled")}`;
|
||||||
const extracting = value.match(/^Entpacken (\d+%)$/);
|
const extracting = value.match(/^Entpacken (\d+%)$/);
|
||||||
if (extracting) return `Extracting ${extracting[1]}`;
|
if (extracting) return `Extracting ${extracting[1]}`;
|
||||||
|
const finalizing = value.match(/^Finalisieren - (\d+%)$/);
|
||||||
|
if (finalizing) return `Finalizing - ${finalizing[1]}`;
|
||||||
const checkedUntil = value.match(/^Account geprüft — (.+) bis (.+)$/);
|
const checkedUntil = value.match(/^Account geprüft — (.+) bis (.+)$/);
|
||||||
if (checkedUntil) return `Account checked — ${checkedUntil[1]} until ${checkedUntil[2]}`;
|
if (checkedUntil) return `Account checked — ${checkedUntil[1]} until ${checkedUntil[2]}`;
|
||||||
const checked = value.match(/^Account geprüft — (.+)$/);
|
const checked = value.match(/^Account geprüft — (.+)$/);
|
||||||
@@ -403,6 +405,8 @@ function translateDynamic(value: string, language: AppLanguage): string {
|
|||||||
if (result) return `${result[1]} fertig${result[2].replace(/(\d+) errors/g, "$1 Fehler").replace(/(\d+) cancelled/g, "$1 abgebrochen")}`;
|
if (result) return `${result[1]} fertig${result[2].replace(/(\d+) errors/g, "$1 Fehler").replace(/(\d+) cancelled/g, "$1 abgebrochen")}`;
|
||||||
const extracting = value.match(/^Extracting (\d+%)$/);
|
const extracting = value.match(/^Extracting (\d+%)$/);
|
||||||
if (extracting) return `Entpacken ${extracting[1]}`;
|
if (extracting) return `Entpacken ${extracting[1]}`;
|
||||||
|
const finalizing = value.match(/^Finalizing - (\d+%)$/);
|
||||||
|
if (finalizing) return `Finalisieren - ${finalizing[1]}`;
|
||||||
const checkedUntil = value.match(/^Account checked — (.+) until (.+)$/);
|
const checkedUntil = value.match(/^Account checked — (.+) until (.+)$/);
|
||||||
if (checkedUntil) return `Account geprüft — ${checkedUntil[1]} bis ${checkedUntil[2]}`;
|
if (checkedUntil) return `Account geprüft — ${checkedUntil[1]} bis ${checkedUntil[2]}`;
|
||||||
const credentialsFor = value.match(/^Credentials for (.+)$/);
|
const credentialsFor = value.match(/^Credentials for (.+)$/);
|
||||||
|
|||||||
@@ -153,6 +153,20 @@ export function compactDownloadStatus(value: string): string {
|
|||||||
if (extracting) return `Entpacken - ${extracting[1]}%`;
|
if (extracting) return `Entpacken - ${extracting[1]}%`;
|
||||||
const extractingEnglish = status.match(/Extracting\s+(\d+)%/i);
|
const extractingEnglish = status.match(/Extracting\s+(\d+)%/i);
|
||||||
if (extractingEnglish) return `Extracting - ${extractingEnglish[1]}%`;
|
if (extractingEnglish) return `Extracting - ${extractingEnglish[1]}%`;
|
||||||
|
const finalizing = status.match(/^(Finalisieren|Finalizing)\b/i);
|
||||||
|
if (finalizing) {
|
||||||
|
const fraction = status.match(/\(([^)]*)\)/);
|
||||||
|
if (fraction) {
|
||||||
|
const [currentValue, totalValue] = fraction[1].split("/");
|
||||||
|
const current = Number(currentValue?.trim());
|
||||||
|
const total = Number(totalValue?.trim());
|
||||||
|
if (Number.isFinite(current) && Number.isFinite(total) && total > 0) return `${finalizing[1]} - ${progress((current / total) * 100)}%`;
|
||||||
|
return finalizing[1];
|
||||||
|
}
|
||||||
|
const percentage = status.match(/-\s*(-?\d+(?:\.\d+)?)%/);
|
||||||
|
if (percentage) return `${finalizing[1]} - ${progress(Number(percentage[1]))}%`;
|
||||||
|
return finalizing[1];
|
||||||
|
}
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,9 +461,10 @@ function packageCell(row: DownloadPackageRow, column: string, packageSpeedBps: n
|
|||||||
if (column === "status") {
|
if (column === "status") {
|
||||||
const audio = entry.audioStripSummary ? formatAudioStripSummary(entry.audioStripSummary) : null;
|
const audio = entry.audioStripSummary ? formatAudioStripSummary(entry.audioStripSummary) : null;
|
||||||
const rawPostProcessLabel = entry.postProcessLabel?.trim() || "";
|
const rawPostProcessLabel = entry.postProcessLabel?.trim() || "";
|
||||||
const postProcessLabel = entry.status === "extracting" && /(?:^|[\\/])[^\\/]+\.(?:rar|zip|7z|tar|gz|bz2|xz)(?:\.\d+)?$/i.test(rawPostProcessLabel)
|
const compactPostProcessLabel = compactDownloadStatus(rawPostProcessLabel);
|
||||||
|
const postProcessLabel = entry.status === "extracting" && compactPostProcessLabel === rawPostProcessLabel && /(?:^|[\\/])[^\\/]+\.(?:rar|zip|7z|tar|gz|bz2|xz)(?:\.\d+)?$/i.test(rawPostProcessLabel)
|
||||||
? "Entpacken - Ausstehend"
|
? "Entpacken - Ausstehend"
|
||||||
: compactDownloadStatus(rawPostProcessLabel);
|
: compactPostProcessLabel;
|
||||||
const extractFailure = row.allItems.find((item) => /^Entpack-Fehler\b/i.test(item.fullStatus || ""));
|
const extractFailure = row.allItems.find((item) => /^Entpack-Fehler\b/i.test(item.fullStatus || ""));
|
||||||
const waitsForDisk = row.allItems.some((item) => compactDownloadStatus(item.fullStatus || "") === "Warte auf Festplatte");
|
const waitsForDisk = row.allItems.some((item) => compactDownloadStatus(item.fullStatus || "") === "Warte auf Festplatte");
|
||||||
const details = `${stats.done}/${stats.total}${stats.failed > 0 ? ` · ${stats.failed} Fehler` : ""}${stats.cancelled > 0 ? ` · ${stats.cancelled} abgebrochen` : ""}${postProcessLabel ? ` · ${postProcessLabel}` : ""}${extractFailure ? " · Entpack-Fehler" : ""}${audio ? ` · ${audio.text}` : ""}`;
|
const details = `${stats.done}/${stats.total}${stats.failed > 0 ? ` · ${stats.failed} Fehler` : ""}${stats.cancelled > 0 ? ` · ${stats.cancelled} abgebrochen` : ""}${postProcessLabel ? ` · ${postProcessLabel}` : ""}${extractFailure ? " · Entpack-Fehler" : ""}${audio ? ` · ${audio.text}` : ""}`;
|
||||||
|
|||||||
@@ -923,6 +923,51 @@ describe("download table row contracts", () => {
|
|||||||
expect(html).not.toContain(">release.part1.rar</span>");
|
expect(html).not.toContain(">release.part1.rar</span>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["package", "Finalisieren (1/2) · release.part1.rar", "Finalisieren - 50%"],
|
||||||
|
["package", "Finalizing - 50% * release.part1.rar", "Finalizing - 50%"],
|
||||||
|
["item", "Finalizing (3/4) · release.part1.rar", "Finalizing - 75%"],
|
||||||
|
["item", "Finalisieren - 50% * release.part1.rar", "Finalisieren - 50%"]
|
||||||
|
])("shows %s finalization status without archive details", (target, rawStatus, expectedStatus) => {
|
||||||
|
const html = target === "package"
|
||||||
|
? renderToStaticMarkup(PackageCardContent({
|
||||||
|
actions: createActions(),
|
||||||
|
columnOrder: ["status"],
|
||||||
|
editing: false,
|
||||||
|
editingName: "",
|
||||||
|
gridTemplate: "220px",
|
||||||
|
packageSpeedBps: 0,
|
||||||
|
row: {
|
||||||
|
package: { ...pkg("finalization-package", "Finalization", ["finalization-item"]), postProcessLabel: rawStatus, status: "extracting" } as PackageEntry,
|
||||||
|
items: [item("finalization-item", "finalization-package", "completed")],
|
||||||
|
allItems: [item("finalization-item", "finalization-package", "completed")],
|
||||||
|
collapsed: true
|
||||||
|
},
|
||||||
|
selectedIds: new Set<string>(),
|
||||||
|
selectedVersion: 0
|
||||||
|
}))
|
||||||
|
: renderToStaticMarkup(ItemRowContent({
|
||||||
|
actions: createActions(),
|
||||||
|
columnOrder: ["status"],
|
||||||
|
gridTemplate: "220px",
|
||||||
|
item: item("finalization-item", "finalization-package", "completed", { fullStatus: rawStatus }),
|
||||||
|
selected: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(html).toContain(`aria-label="${expectedStatus}"`);
|
||||||
|
expect(html.match(new RegExp(`>${expectedStatus}</span>`, "g"))).toHaveLength(2);
|
||||||
|
expect(html).not.toContain(">release.part1.rar</span>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["Finalisieren (3/2) · release.part1.rar", "Finalisieren - 100%"],
|
||||||
|
["Finalizing (-1/2) · release.part1.rar", "Finalizing - 0%"],
|
||||||
|
["Finalisieren (1/0) · release.part1.rar", "Finalisieren"],
|
||||||
|
["Finalizing (invalid/2) · release.part1.rar", "Finalizing"]
|
||||||
|
])("derives only finite finalization percentages", (rawStatus, expectedStatus) => {
|
||||||
|
expect(compactDownloadStatus(rawStatus)).toBe(expectedStatus);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders meter text in clipped track and fill layers", () => {
|
it("renders meter text in clipped track and fill layers", () => {
|
||||||
const html = renderToStaticMarkup(ItemRowContent({
|
const html = renderToStaticMarkup(ItemRowContent({
|
||||||
actions: createActions(),
|
actions: createActions(),
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ describe("renderer localization", () => {
|
|||||||
expect(translateUiText("100.001–100.005 of 100.005", "de")).toBe("100.001–100.005 von 100.005");
|
expect(translateUiText("100.001–100.005 of 100.005", "de")).toBe("100.001–100.005 von 100.005");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["Finalisieren - 0%", "Finalizing - 0%"],
|
||||||
|
["Finalisieren - 50%", "Finalizing - 50%"],
|
||||||
|
["Finalisieren - 100%", "Finalizing - 100%"]
|
||||||
|
])("translates compact finalization progress %s in both directions", (german, english) => {
|
||||||
|
expect(translateUiText(german, "en")).toBe(english);
|
||||||
|
expect(translateUiText(english, "de")).toBe(german);
|
||||||
|
});
|
||||||
|
|
||||||
it("translates the complete history surface including status values", () => {
|
it("translates the complete history surface including status values", () => {
|
||||||
const translations = new Map([
|
const translations = new Map([
|
||||||
["Alle Einträge", "All entries"],
|
["Alle Einträge", "All entries"],
|
||||||
|
|||||||
Reference in New Issue
Block a user