feat(ui): add download progress controls

Replace the downloading action button with a dedicated progress component in
both card and detail views. The card now shows percent plus current speed, while
the detail modal shows bytes, speed, ETA, percent, and an inline cancel affordance
using the same backend progress payload.

Expose download cancellation as a peer command that cancels the tracked transfer
token and lets the running operation clear the authoritative active-operation
snapshot. Add a View Files action that resolves the game root safely and opens it
with the platform file viewer through Tauri's shell plugin.

Test Plan:
- just fmt
- just frontend-test
- just test
- just build
- just clippy
- git diff --cached --check

Refs: design reference e308009a08
This commit is contained in:
2026-05-20 23:20:53 +02:00
parent e308009a08
commit 47e2bbd454
16 changed files with 776 additions and 48 deletions
@@ -629,6 +629,11 @@
background: var(--warn);
box-shadow: 0 0 8px var(--warn);
}
.state-chip[data-state="downloading"] .state-dot {
background: var(--accent);
box-shadow: 0 0 8px var(--accent);
animation: state-busy 1.2s ease-in-out infinite;
}
.state-chip[data-state="busy"] .state-dot {
background: var(--accent);
box-shadow: 0 0 8px var(--accent);
@@ -807,20 +812,6 @@
background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--bd-1);
}
.act-downloading {
min-width: 148px;
font-variant-numeric: tabular-nums;
}
.act-lg.act-downloading {
min-width: 174px;
}
.act-progress-fill {
position: absolute;
inset: 0 auto 0 0;
width: var(--download-progress, 0%);
background: color-mix(in srgb, var(--accent) 28%, transparent);
transition: width 0.45s linear;
}
.act-busy::before {
content: "";
display: inline-block;
@@ -846,6 +837,264 @@
cursor: not-allowed;
}
/* Download progress */
.dl {
position: relative;
overflow: hidden;
border-radius: 7px;
border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--bd-2));
background: rgba(255, 255, 255, 0.04);
color: var(--t-1);
font: inherit;
font-variant-numeric: tabular-nums;
container-type: inline-size;
isolation: isolate;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.04),
0 0 0 1px color-mix(in srgb, var(--accent) 16%, transparent);
}
.dl-full {
width: 100%;
}
.dl-fill {
position: absolute;
inset: 0 auto 0 0;
z-index: 0;
width: var(--download-progress, 0%);
background: linear-gradient(
180deg,
color-mix(in srgb, var(--accent) 38%, transparent) 0%,
color-mix(in srgb, var(--accent) 26%, transparent) 100%
);
border-right: 1px solid color-mix(in srgb, var(--accent) 75%, transparent);
box-shadow: 2px 0 8px color-mix(in srgb, var(--accent) 35%, transparent);
transition: width 0.48s cubic-bezier(0.4, 0, 0.2, 1);
}
.dl-fill::after {
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(
115deg,
transparent 0 14px,
rgba(255, 255, 255, 0.05) 14px 22px
);
background-size: 200% 100%;
animation: dl-stripe 1.4s linear infinite;
mix-blend-mode: screen;
opacity: 0.85;
}
@keyframes dl-stripe {
from {
background-position: 0 0;
}
to {
background-position: -36px 0;
}
}
.dl-pulse {
width: 7px;
height: 7px;
border-radius: 999px;
background: var(--accent);
box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 70%, transparent);
animation: dl-pulse 1.4s ease-out infinite;
flex: 0 0 auto;
}
@keyframes dl-pulse {
0% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 70%, transparent);
}
70% {
box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 0%, transparent);
}
100% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent);
}
}
.dl-md {
height: 32px;
padding: 0 10px;
}
.dl-md-row {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 100%;
gap: 8px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0;
}
.dl-md .dl-pct {
display: inline-flex;
align-items: center;
color: var(--t-1);
}
.dl-md .dl-pulse {
margin-right: 6px;
}
.dl-pct-sym {
opacity: 0.55;
font-weight: 600;
margin-left: 1px;
}
.dl-md .dl-speed {
color: var(--t-2);
font-size: 11px;
font-weight: 500;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
@container (max-width: 132px) {
.dl-md .dl-speed {
display: none;
}
.dl-md-row {
justify-content: center;
gap: 6px;
}
}
@container (max-width: 96px) {
.dl-md .dl-pulse {
display: none;
}
}
.density-compact .dl-md {
height: 30px;
padding: 0 9px;
}
.density-compact .dl-md-row {
font-size: 11.5px;
}
.density-compact .dl-md .dl-speed {
font-size: 10.5px;
}
.density-large .dl-md {
height: 34px;
padding: 0 12px;
}
.density-large .dl-md-row {
font-size: 13px;
}
.density-large .dl-md .dl-speed {
font-size: 11.5px;
}
.dl-lg {
height: 56px;
padding: 0;
border-radius: 9px;
flex: 1 1 auto;
min-width: 260px;
}
.dl-lg-grid {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
grid-template-rows: auto auto;
grid-template-areas:
"primary pct cancel"
"secondary pct cancel";
align-items: center;
height: 100%;
padding: 0 14px 0 16px;
column-gap: 14px;
row-gap: 2px;
}
.dl-lg-primary {
grid-area: primary;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
font-size: 13px;
font-weight: 600;
letter-spacing: 0;
text-transform: uppercase;
color: color-mix(in srgb, var(--accent) 80%, white);
}
.dl-lg-primary .dl-label {
white-space: nowrap;
}
.dl-lg-secondary {
grid-area: secondary;
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
overflow: hidden;
font-size: 12px;
font-weight: 500;
color: var(--t-2);
}
.dl-lg-secondary .dl-bytes {
color: var(--t-1);
font-weight: 600;
white-space: nowrap;
}
.dl-lg-secondary .dl-of {
color: var(--t-2);
font-weight: 500;
}
.dl-lg-secondary .dl-speed {
color: var(--t-1);
font-weight: 600;
white-space: nowrap;
}
.dl-lg-secondary .dl-eta {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dl-sep {
opacity: 0.45;
}
@container (max-width: 380px) {
.dl-lg-secondary .dl-eta,
.dl-lg-secondary .dl-sep-eta {
display: none;
}
}
.dl-lg-pct {
grid-area: pct;
color: var(--t-1);
font-size: 20px;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: 0;
line-height: 1;
}
.dl-lg-pct .dl-pct-sym {
font-size: 12px;
}
.dl-cancel {
grid-area: cancel;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid var(--bd-2);
background: rgba(255, 255, 255, 0.04);
color: var(--t-2);
cursor: pointer;
transition:
background 0.15s,
border-color 0.15s,
color 0.15s;
}
.dl-cancel:hover {
background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.4);
color: #fca5a5;
}
/* Ghost / secondary buttons */
.ghost-btn {
display: inline-flex;