Refine statistics overview layout

This commit is contained in:
Sucukdeluxe 2026-03-09 05:53:05 +01:00
parent 914bbcc59a
commit 05df79d518
2 changed files with 168 additions and 23 deletions

View File

@ -3790,6 +3790,52 @@ export function App(): ReactElement {
: 0;
const liveSessionRuntimeMs = Math.max(0, (snapshot.stats.sessionRuntimeMs || 0) + runtimeOffsetMs);
const liveTotalRuntimeMs = Math.max(0, (snapshot.stats.totalRuntimeMs || 0) + runtimeOffsetMs);
const etaSeparatorIndex = snapshot.etaText.includes(": ") ? snapshot.etaText.indexOf(": ") : -1;
const etaValue = etaSeparatorIndex >= 0
? snapshot.etaText.slice(etaSeparatorIndex + 2)
: "--";
const resetFailedDownloads = (): void => {
if (itemStatusCounts.failed === 0) return;
const failedIds = Object.values(snapshot.session.items)
.filter((it) => it.status === "failed")
.map((it) => it.id);
void window.rd.resetItems(failedIds).catch(() => {});
};
const statsSections = [
{
key: "live",
title: "Aktuell",
items: [
{ key: "speed", eyebrow: "Live", label: "Geschwindigkeit", value: snapshot.speedText.replace("Geschwindigkeit: ", "") },
{ key: "eta", eyebrow: "Live", label: "Restzeit", value: etaValue, compactValue: true },
{ key: "active", eyebrow: "Queue", label: "Aktive Downloads", value: String(itemStatusCounts.downloading) },
{ key: "queued", eyebrow: "Queue", label: "In Warteschlange", value: String(itemStatusCounts.queued) },
{
key: "failed",
eyebrow: "Status",
label: "Fehlerhaft",
value: String(itemStatusCounts.failed),
danger: itemStatusCounts.failed > 0,
clickable: itemStatusCounts.failed > 0,
title: itemStatusCounts.failed > 0 ? "Klicken zum Zurücksetzen aller fehlerhaften Downloads" : undefined,
onClick: resetFailedDownloads
},
{ key: "packages", eyebrow: "Queue", label: "Pakete", value: String(snapshot.stats.totalPackages) }
]
},
{
key: "summary",
title: "Bilanz",
items: [
{ key: "downloaded-session", eyebrow: "Session", label: "Heruntergeladen", value: humanSize(snapshot.stats.totalDownloaded) },
{ key: "downloaded-total", eyebrow: "Gesamt", label: "Heruntergeladen", value: humanSize(snapshot.stats.totalDownloadedAllTime) },
{ key: "runtime-session", eyebrow: "Session", label: "Laufzeit", value: formatRuntimeDuration(liveSessionRuntimeMs), compactValue: true },
{ key: "runtime-total", eyebrow: "Gesamt", label: "Laufzeit", value: formatRuntimeDuration(liveTotalRuntimeMs), compactValue: true },
{ key: "files-session", eyebrow: "Session", label: "Fertige Dateien", value: String(snapshot.stats.totalFilesSession) },
{ key: "files-total", eyebrow: "Gesamt", label: "Fertige Dateien", value: String(snapshot.stats.totalFilesAllTime) }
]
}
] as const;
return (
<div
@ -4434,19 +4480,38 @@ export function App(): ReactElement {
});
}}>Heruntergeladen zurücksetzen</button>
</div>
<div className="stats-grid">
<div className="stat-item">
<span className="stat-label">Aktuelle Geschwindigkeit</span>
<span className="stat-value">{snapshot.speedText.replace("Geschwindigkeit: ", "")}</span>
</div>
<div className="stat-item">
<span className="stat-label">Heruntergeladen (Session)</span>
<span className="stat-value">{humanSize(snapshot.stats.totalDownloaded)}</span>
</div>
<div className="stat-item">
<span className="stat-label">Heruntergeladen (Gesamt)</span>
<span className="stat-value">{humanSize(snapshot.stats.totalDownloadedAllTime)}</span>
</div>
<div className="stats-sections">
{statsSections.map((section) => (
<section key={section.key} className="stats-section">
<div className="stats-section-title">{section.title}</div>
<div className="stats-grid">
{section.items.map((item) => item.onClick ? (
<button
key={item.key}
type="button"
className={`stat-item stat-button${item.clickable ? " stat-item-clickable" : ""}`}
title={item.title}
onClick={item.onClick}
>
<span className="stat-top">
<span className="stat-eyebrow">{item.eyebrow}</span>
<span className="stat-label">{item.label}</span>
</span>
<span className={`stat-value${item.danger ? " danger" : ""}${item.compactValue ? " stat-value-compact" : ""}`}>{item.value}</span>
</button>
) : (
<div key={item.key} className="stat-item">
<span className="stat-top">
<span className="stat-eyebrow">{item.eyebrow}</span>
<span className="stat-label">{item.label}</span>
</span>
<span className={`stat-value${item.compactValue ? " stat-value-compact" : ""}`}>{item.value}</span>
</div>
))}
</div>
</section>
))}
{false && (<>
<div className="stat-item">
<span className="stat-label">Laufzeit (Session)</span>
<span className="stat-value">{formatRuntimeDuration(liveSessionRuntimeMs)}</span>
@ -4493,6 +4558,7 @@ export function App(): ReactElement {
<span className="stat-label">{snapshot.etaText.includes(": ") ? snapshot.etaText.slice(0, snapshot.etaText.indexOf(": ")) : snapshot.etaText}</span>
<span className="stat-value">{snapshot.etaText.includes(": ") ? snapshot.etaText.slice(snapshot.etaText.indexOf(": ") + 2) : "--"}</span>
</div>
</>)}
</div>
</article>
@ -6120,4 +6186,3 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, stripe
}
return true;
});

View File

@ -2501,39 +2501,97 @@ td {
margin-top: 10px;
}
.stats-sections {
display: flex;
flex-direction: column;
gap: 14px;
margin-top: 14px;
}
.stats-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.stats-section-title {
color: var(--muted);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 12px;
margin-top: 8px;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
justify-content: space-between;
gap: 12px;
min-height: 96px;
min-width: 0;
padding: 12px 14px;
background: var(--field);
border-radius: 10px;
border: 1px solid var(--border);
text-align: left;
}
.stat-button {
width: 100%;
font: inherit;
color: inherit;
appearance: none;
cursor: default;
}
.stat-top {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.stat-eyebrow {
color: var(--muted);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.stat-item.stat-item-clickable {
cursor: pointer;
transition: border-color 0.15s;
transition: border-color 0.15s, transform 0.15s ease;
}
.stat-item.stat-item-clickable:hover {
border-color: var(--danger, #e05555);
transform: translateY(-1px);
}
.stat-label {
color: var(--muted);
font-size: 12px;
color: var(--text);
font-size: 14px;
font-weight: 600;
line-height: 1.25;
}
.stat-value {
font-size: 18px;
font-size: clamp(22px, 1.35vw, 30px);
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1.1;
overflow-wrap: anywhere;
}
.stat-value.stat-value-compact {
font-size: clamp(18px, 1.05vw, 24px);
line-height: 1.2;
}
.stat-value.danger {
@ -2629,6 +2687,12 @@ td {
font-size: 13px;
}
@media (max-width: 1680px) {
.stats-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 1100px) {
.statistics-view {
grid-template-columns: 1fr;
@ -2640,7 +2704,23 @@ td {
}
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.stats-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.stat-item {
min-height: 88px;
}
}
@media (max-width: 520px) {
.stats-grid {
grid-template-columns: 1fr;
}
}