feat(notifications): finalize package and run results
This commit is contained in:
@@ -20,6 +20,7 @@ import { Toolbar, ToolbarGroup, ToolbarSearch } from "../../ui/Toolbar";
|
||||
import { SlidingSelection } from "../../ui/SlidingSelection";
|
||||
import {
|
||||
createHistoryTableColumnWidths,
|
||||
formatHistoryDuration,
|
||||
getHistoryTableGridTemplate,
|
||||
getHistoryTableMinWidth,
|
||||
HISTORY_TABLE_COLUMN_IDS,
|
||||
@@ -69,6 +70,12 @@ const HISTORY_DISCLOSURE_DURATION_MS = 520;
|
||||
const useRendererLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
||||
let historyTableResizeSession: { column: HistoryTableColumnId; startX: number; initial: HistoryTableColumnWidths } | null = null;
|
||||
|
||||
function operationStatusLabel(status: "completed" | "failed" | "cancelled"): string {
|
||||
if (status === "completed") return "Abgeschlossen";
|
||||
if (status === "failed") return "Fehlgeschlagen";
|
||||
return "Abgebrochen";
|
||||
}
|
||||
|
||||
function loadHistoryTableColumnWidths(): HistoryTableColumnWidths {
|
||||
try {
|
||||
const stored = typeof window === "undefined" ? null : window.localStorage.getItem(HISTORY_TABLE_COLUMN_STORAGE_KEY);
|
||||
@@ -183,11 +190,59 @@ function HistoryRowDetails({
|
||||
<dl className="history-details-grid">
|
||||
<div><dt>Provider</dt><dd>{row.providerLabel}</dd></div>
|
||||
<div><dt>Dateien</dt><dd>{row.fileCount}</dd></div>
|
||||
<div><dt>Dauer</dt><dd>{row.durationLabel}</dd></div>
|
||||
{row.hasStructuredLifecycle ? (
|
||||
<>
|
||||
<div><dt>Download gestartet</dt><dd>{row.startedLabel}</dd></div>
|
||||
<div><dt>Download beendet</dt><dd>{row.downloadEndedLabel}</dd></div>
|
||||
<div><dt>Nachbearbeitung gestartet</dt><dd>{row.postProcessStartedLabel}</dd></div>
|
||||
<div><dt>Abgeschlossen</dt><dd>{row.completedLabel}</dd></div>
|
||||
<div><dt>Downloaddauer</dt><dd>{row.downloadDurationLabel}</dd></div>
|
||||
<div><dt>Entpackdauer</dt><dd>{row.extractionDurationLabel}</dd></div>
|
||||
<div><dt>Remuxdauer</dt><dd>{row.remuxDurationLabel}</dd></div>
|
||||
<div><dt>Nachbearbeitungsdauer</dt><dd>{row.postProcessDurationLabel}</dd></div>
|
||||
<div><dt>Gesamtdauer</dt><dd>{row.totalDurationLabel}</dd></div>
|
||||
<div><dt>Status</dt><dd>{row.statusLabel}</dd></div>
|
||||
<div><dt>Erfolgreich / Fehlgeschlagen / Abgebrochen</dt><dd>{row.successfulFiles ?? 0} / {row.failedFiles ?? 0} / {row.cancelledFiles ?? 0}</dd></div>
|
||||
<div><dt>Archive / Parts / Ausgaben</dt><dd>{row.archiveCount ?? 0} / {row.partCount ?? 0} / {row.outputCount ?? 0}</dd></div>
|
||||
<div><dt>Fehlerphase</dt><dd>{row.failurePhaseLabel}</dd></div>
|
||||
</>
|
||||
) : (
|
||||
<div><dt>Downloaddauer (Altbestand)</dt><dd>{row.durationLabel}</dd></div>
|
||||
)}
|
||||
<div><dt>Durchschnitt</dt><dd>{row.averageSpeedLabel}</dd></div>
|
||||
<div className="history-detail-wide"><dt>Zielordner</dt><dd className="history-copyable">{row.outputDir || "—"}</dd></div>
|
||||
<div className="history-detail-wide"><dt>URLs</dt><dd className="history-copyable">{row.urls?.length ? row.urls.join("\n") : "—"}</dd></div>
|
||||
</dl>
|
||||
{row.hasStructuredLifecycle ? (
|
||||
<div className="history-operation-groups">
|
||||
<section className="history-operation-group">
|
||||
<h3>Archivvorgänge</h3>
|
||||
{row.archiveOperations?.length ? (
|
||||
<ul>
|
||||
{row.archiveOperations.map((operation) => (
|
||||
<li key={operation.id}>
|
||||
<strong>{operation.name}</strong>
|
||||
<span>{operation.partCount} Parts · {formatHistoryDuration(operation.durationMs / 1000)} · {operationStatusLabel(operation.status)}{operation.errorCategory ? ` · ${operation.errorCategory}` : ""}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : <p>Keine Archivvorgänge</p>}
|
||||
</section>
|
||||
<section className="history-operation-group">
|
||||
<h3>Remuxvorgänge</h3>
|
||||
{row.remuxOperations?.length ? (
|
||||
<ul>
|
||||
{row.remuxOperations.map((operation) => (
|
||||
<li key={operation.id}>
|
||||
<strong>{operation.fileName}</strong>
|
||||
<span>{formatHistoryDuration(operation.durationMs / 1000)} · {operationStatusLabel(operation.status)}{operation.errorCategory ? ` · ${operation.errorCategory}` : ""}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : <p>Keine Remuxvorgänge</p>}
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,15 @@ export interface HistoryRow extends HistoryViewEntry {
|
||||
durationLabel: string;
|
||||
averageSpeedLabel: string;
|
||||
statusLabel: string;
|
||||
hasStructuredLifecycle: boolean;
|
||||
downloadEndedLabel: string;
|
||||
postProcessStartedLabel: string;
|
||||
downloadDurationLabel: string;
|
||||
extractionDurationLabel: string;
|
||||
remuxDurationLabel: string;
|
||||
postProcessDurationLabel: string;
|
||||
totalDurationLabel: string;
|
||||
failurePhaseLabel: string;
|
||||
}
|
||||
|
||||
export interface HistoryFilterCounts {
|
||||
@@ -147,7 +156,7 @@ function formatBytes(bytes: number): string {
|
||||
return `${numberFormatter.format(value)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function formatDuration(durationSeconds: number): string {
|
||||
export function formatHistoryDuration(durationSeconds: number): string {
|
||||
const total = Math.max(0, Math.floor(Number.isFinite(durationSeconds) ? durationSeconds : 0));
|
||||
const hours = Math.floor(total / 3600);
|
||||
const minutes = Math.floor((total % 3600) / 60);
|
||||
@@ -158,6 +167,19 @@ function formatDuration(durationSeconds: number): string {
|
||||
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp: number | undefined): string {
|
||||
const safe = Math.max(0, Number.isFinite(timestamp) ? Number(timestamp) : 0);
|
||||
return safe > 0 ? dateFormatter.format(new Date(safe)) : "—";
|
||||
}
|
||||
|
||||
function failurePhaseLabel(entry: HistoryViewEntry): string {
|
||||
if (entry.failurePhase === "download") return "Download";
|
||||
if (entry.failurePhase === "extract") return "Entpacken";
|
||||
if (entry.failurePhase === "remux") return "Remux";
|
||||
if (entry.failurePhase === "cleanup") return "Aufräumen";
|
||||
return "—";
|
||||
}
|
||||
|
||||
export function paginateHistoryRows(rows: HistoryRow[], requestedPage: number): HistoryPage {
|
||||
const totalItems = rows.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalItems / HISTORY_PAGE_SIZE));
|
||||
@@ -234,7 +256,11 @@ export function deriveHistoryHoster(urls: string[] | undefined): string {
|
||||
return hostnames.length > 0 ? hostnames.join(", ") : "—";
|
||||
}
|
||||
|
||||
export function deriveHistoryStartAt(entry: Pick<HistoryViewEntry, "completedAt" | "durationSeconds">): number {
|
||||
export function deriveHistoryStartAt(entry: Pick<HistoryViewEntry, "completedAt" | "durationSeconds" | "startedAt">): number {
|
||||
const startedAt = Math.max(0, Number.isFinite(entry.startedAt) ? Number(entry.startedAt) : 0);
|
||||
if (startedAt > 0) {
|
||||
return startedAt;
|
||||
}
|
||||
const completedAt = Math.max(0, Number.isFinite(entry.completedAt) ? entry.completedAt : 0);
|
||||
const durationMs = Math.max(0, Number.isFinite(entry.durationSeconds) ? entry.durationSeconds : 0) * 1000;
|
||||
return Math.max(0, completedAt - durationMs);
|
||||
@@ -244,19 +270,32 @@ function toHistoryRow(entry: HistoryViewEntry): HistoryRow {
|
||||
const hoster = deriveHistoryHoster(entry.urls);
|
||||
const providerLabel = entry.provider ? providerLabels[entry.provider] : "—";
|
||||
const startAt = deriveHistoryStartAt(entry);
|
||||
const durationSeconds = Math.max(0, entry.durationSeconds || 0);
|
||||
const averageBytesPerSecond = durationSeconds > 0 ? entry.downloadedBytes / durationSeconds : 0;
|
||||
const downloadDurationSeconds = Math.max(0, entry.downloadDurationSeconds ?? entry.durationSeconds ?? 0);
|
||||
const averageBytesPerSecond = downloadDurationSeconds > 0 ? entry.downloadedBytes / downloadDurationSeconds : 0;
|
||||
const hasStructuredLifecycle = entry.startedAt !== undefined
|
||||
|| entry.downloadEndedAt !== undefined
|
||||
|| entry.postProcessStartedAt !== undefined
|
||||
|| entry.totalDurationSeconds !== undefined;
|
||||
return {
|
||||
...entry,
|
||||
hoster,
|
||||
providerLabel,
|
||||
startAt,
|
||||
sizeLabel: `${formatBytes(entry.downloadedBytes)} / ${formatBytes(entry.totalBytes)}`,
|
||||
startedLabel: dateFormatter.format(new Date(startAt)),
|
||||
completedLabel: dateFormatter.format(new Date(Math.max(0, entry.completedAt))),
|
||||
durationLabel: formatDuration(durationSeconds),
|
||||
averageSpeedLabel: durationSeconds > 0 ? `${formatBytes(averageBytesPerSecond)}/s` : "—",
|
||||
statusLabel: statusLabels[entry.status]
|
||||
startedLabel: formatTimestamp(startAt),
|
||||
completedLabel: formatTimestamp(entry.completedAt),
|
||||
durationLabel: formatHistoryDuration(downloadDurationSeconds),
|
||||
averageSpeedLabel: downloadDurationSeconds > 0 ? `${formatBytes(averageBytesPerSecond)}/s` : "—",
|
||||
statusLabel: statusLabels[entry.status],
|
||||
hasStructuredLifecycle,
|
||||
downloadEndedLabel: formatTimestamp(entry.downloadEndedAt),
|
||||
postProcessStartedLabel: formatTimestamp(entry.postProcessStartedAt),
|
||||
downloadDurationLabel: formatHistoryDuration(entry.downloadDurationSeconds ?? 0),
|
||||
extractionDurationLabel: formatHistoryDuration(entry.extractionDurationSeconds ?? 0),
|
||||
remuxDurationLabel: formatHistoryDuration(entry.remuxDurationSeconds ?? 0),
|
||||
postProcessDurationLabel: formatHistoryDuration(entry.postProcessDurationSeconds ?? 0),
|
||||
totalDurationLabel: formatHistoryDuration(entry.totalDurationSeconds ?? 0),
|
||||
failurePhaseLabel: failurePhaseLabel(entry)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -339,11 +339,23 @@
|
||||
color: var(--ui-danger-text);
|
||||
}
|
||||
|
||||
.history-status-failed {
|
||||
background: color-mix(in srgb, var(--ui-danger) 15%, transparent);
|
||||
border-color: color-mix(in srgb, var(--ui-danger) 60%, var(--ui-border));
|
||||
.history-status-failed {
|
||||
background: color-mix(in srgb, var(--ui-danger) 15%, transparent);
|
||||
border-color: color-mix(in srgb, var(--ui-danger) 60%, var(--ui-border));
|
||||
color: var(--ui-danger-text);
|
||||
}
|
||||
}
|
||||
|
||||
.history-status-partial {
|
||||
background: color-mix(in srgb, var(--ui-warning) 16%, transparent);
|
||||
border-color: color-mix(in srgb, var(--ui-warning) 60%, var(--ui-border));
|
||||
color: var(--ui-warning-text);
|
||||
}
|
||||
|
||||
.history-status-cancelled {
|
||||
background: color-mix(in srgb, var(--ui-text-muted) 14%, transparent);
|
||||
border-color: color-mix(in srgb, var(--ui-text-muted) 48%, var(--ui-border));
|
||||
color: var(--ui-text-secondary);
|
||||
}
|
||||
|
||||
.history-row-size,
|
||||
.history-row-hoster,
|
||||
@@ -422,9 +434,55 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-details-grid .history-detail-wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.history-details-grid .history-detail-wide {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.history-operation-groups {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.history-operation-group {
|
||||
background: color-mix(in srgb, var(--ui-panel) 72%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--ui-border) 82%, transparent);
|
||||
border-radius: 6px;
|
||||
min-width: 0;
|
||||
padding: 10px 11px;
|
||||
}
|
||||
|
||||
.history-operation-group h3 {
|
||||
color: var(--ui-text-muted);
|
||||
font-size: 11px;
|
||||
margin: 0 0 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.history-operation-group p,
|
||||
.history-operation-group ul {
|
||||
color: var(--ui-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.history-operation-group ul {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.history-operation-group li {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-operation-group strong,
|
||||
.history-operation-group span {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.history-copyable {
|
||||
overflow-wrap: anywhere;
|
||||
@@ -494,9 +552,13 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.history-action {
|
||||
padding: 0 9px;
|
||||
}
|
||||
.history-action {
|
||||
padding: 0 9px;
|
||||
}
|
||||
|
||||
.history-operation-groups {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user