feat(downloads): status bar with combined speed and ETA - #309
Open
zohaiblazuli wants to merge 3 commits into
Open
feat(downloads): status bar with combined speed and ETA#309zohaiblazuli wants to merge 3 commits into
zohaiblazuli wants to merge 3 commits into
Conversation
The store already tracked per-item speed and ETA, but nothing combined them, so a caller outside /downloads had no way to ask "how fast is the queue moving, and when is it done". getAggregate() answers that. Speed is the sum over downloading items. ETA prefers the batch model — total remaining over total speed — which is far steadier than taking the slowest item, and falls back to the worst per-item ETA when any item has an unknown size (livestreams, course items). It returns null rather than Infinity or NaN when everything is paused or stalled, so formatEta renders nothing instead of nonsense. getAggregateSpeedHistory() keeps a sampled ring buffer of the combined rate, capped at the same 60 points as the per-item history, so it drops straight into DownloadSpeedGraph. Co-Authored-By: Claude <noreply@anthropic.com>
Adds downloads.status_bar.* and regenerates keys.ts. Translations are included for all ten locales because scripts/generate-i18n-keys.js --strict refuses to run when any locale drifts from en.json, so an English-only change fails CI. They follow each file's existing downloads.filter.* wording, but a native speaker should feel free to correct them. Co-Authored-By: Claude <noreply@anthropic.com>
Speed and ETA only existed on /downloads. Leave that page and the only signal left was a badge count, which is the one thing a download manager should never make you go looking for. Browsers solved this with a shelf; this is that. A slim bar sits below the content, showing the combined speed, the combined ETA, overall progress and the existing sparkline. It appears when the queue has work and hides about two seconds after it drains, so it costs nothing when idle. The grace period keeps it from flickering as one item finishes and the next starts. It is a flow sibling inside .shell-body rather than a fixed overlay, so .content simply shrinks and nothing needs padding compensation. Hidden in OmniDisc's immersive mode, alongside the sidebar. Accessibility was the fiddly part. Speed changes every frame, so a live region over the readout would make a screen reader talk without stopping. Instead the numbers are inert, the track is a progressbar whose aria-valuenow is a rounded integer, and one hidden live region announces only milestones — appearance, item count, each 25%, and completion. The enter animation is CSS keyframes, not a Svelte transition, so the global reduce-motion rule actually reaches it. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a slim download status bar that appears whenever the queue has work and hides shortly after it drains. It shows the combined transfer speed, the combined ETA, overall progress and the existing sparkline — from any route in the app.
Why
Speed and ETA already existed, but only on
/downloads. Leave that page and the only remaining signal is the sidebar badge count, so answering "how fast is this going, and when will it be done" means navigating away from whatever you were doing. Browsers solved this with a download shelf; this is the same idea.The gap was never the data.
QueueItemProgressalready carriesspeed_bytes_per_sec,downloaded_bytes,total_bytesandeta_seconds, and the frontend already hasformatSpeed/formatEta/formatBytesplusDownloadSpeedGraph. What was missing was an aggregate selector and somewhere app-wide to mount it.How it works
getAggregate()indownload-store.svelte.ts, alongsidegetCounts(). Speed is the sum overdownloadingitems. ETA prefers the batch model — total remaining over total speed — which is much steadier than taking the slowest item, and falls back to the worst per-item ETA when any active item has an unknown size (livestreams, course items, which carrybytesDownloadedbut no total). It returnsnullrather thanInfinityorNaNwhen everything is paused or stalled, soformatEtarenders nothing and the component drops the segment entirely.getAggregateSpeedHistory()keeps a sampled ring buffer of the combined rate, capped at the same 60 points as the per-item history, so it feedsDownloadSpeedGraphunchanged.DownloadStatusBar.sveltereads the store directly, the same way/downloadsdoes. It reuses the.progress/.progress-fillprimitives rather than introducing a track of its own, switches to.indeterminatewith a byte counter when the total size is unknown, and uses only existing semantic tokens — so it themes correctly across all the theme blocks without adding a colour.Mounting is a flow sibling after
</main>inside.shell-body, not a fixed overlay. Since.shell-bodyis a column flex container and.contentisflex: 1; overflow-y: auto, the bar claims its own row and the content area simply shrinks — no padding compensation, no z-index, no occlusion. It is absent from the.stream-popoutbranch for free, and hidden in OmniDisc's immersive mode alongside the sidebar.A ~2s grace period before hiding stops the bar flickering as one item finishes and the next starts.
Accessibility
This was the fiddly part, and worth calling out for review.
Speed changes every frame, so an
aria-liveregion over the readout would make a screen reader talk continuously. Instead:role="progressbar"witharia-valuenowrounded to an integer, so it changes ~100 times per download rather than per frame, and omitsaria-valuenowwith anaria-valuetextwhen the size is unknownaria-live="polite"region announces milestones only — appearance, item-count change, each 25% crossing, completion — driven by a$derivedkey that only changes at those thresholds@keyframes, not a Sveltetransition:, because the global[data-reduce-motion="true"]rule reaches CSS but not JS-driven transitions:focus-visiblering onlyNotes for maintainers
No Rust changes.
queue.rs,events.rsanddownload-listener.tsare untouched — the events already carried everything needed. Since the diff is frontend-only I did not run thecargo fmt/clippy/teststeps from CONTRIBUTING; nothing insrc-tauri/changed.Translations are included for all ten locales. I would have left this English-only and let translators follow, but
scripts/generate-i18n-keys.js --strictaborts when any locale drifts fromen.json, so an English-only change failspnpm check:i18n. They follow each file's existingdownloads.filter.*wording. Please treat them as best-effort and correct anything that reads wrong — I would rather you rewrite them than ship an awkward string. Happy to drop them to English-only if you would prefer translators own these keys and want to relax the gate for new ones.fa.jsonwas the one judgement call: itsdownloadsblock is still English, but itsnavblock is Persian, so I translated rather than leaving English in a file that is partly localised. Say the word if the convention is the opposite.Not included: a settings toggle for visibility. Auto-hide means the bar costs nothing when idle, so it seemed like a setting nobody would need to find. It is a small addition to
DownloadSettingswith#[serde(default)]if you want one.Verification
Run on Windows 11:
pnpm test— 14 files, 106 passed, including 7 newgetAggregate()cases (empty queue, summed speeds, unknown-size fallback, all-paused →nullETA, excluded statuses, mixed course + generic, history cap)pnpm check— 1433 files, 0 errors; the changed files add no new warningspnpm check:i18n— all 10 locales in syncpnpm build— cleanpnpm tauri devbuild: bar appears on queueing, stays live while navigating between routes, indeterminate state on unknown-size items, noInfinitywhen paused, auto-hides after the queue drainsI have not tested on macOS or Linux. The change is CSS and Svelte only with no platform-specific code, but the flex interaction with
.shell-bodyis worth a glance on a WebKit webview.Happy to split this up, rename anything, or change the layout if it does not match where you want the app to go.
🤖 Generated with Claude Code