Commit Graph
467 Commits
Author SHA1 Message Date
vicrodh cc50b01dca fix(myqbz): mixtape/collection detail row corrections (#766)
- A song row opens the album it is on (its resolved, source-aware albumId)
  instead of a blank page; the album id is plumbed from the first resolved track
  through the resolve pass, and an unresolved click is ignored like the artist
  link. (#766 problem 1)
- The row menu says "Remove from Mixtape" on a mixtape, "Remove from collection"
  elsewhere. (#766 problem 2)
- A playlist inside a mixtape hides its quality cell (per-track quality is
  varied); collections keep it.
- The source column shows one glyph per DISTINCT source across the item's
  tracks, so a mixed-source playlist shows them all.
- The tracks column is empty only for a track; an album or playlist shows its
  resolved track count instead of a dash.

cc
2026-09-10 16:11:44 -06:00
vicrodh 89da9bbd13 feat(scene): loading heads-up and a 5-minute retry timeout
The ArtistScene discovery fans out many MusicBrainz requests, so a throttled MB
can keep it loading a while. After ~10s the loading screen shows a "this can
take a few minutes" note; after 5 minutes it swaps the spinner for a Retry (a
late success still swaps in the grid on its own). Both derive from the existing
pulse-driven clock, so no new Timer and the repaint law holds. Two new strings
across all eight locales.

cc
2026-09-10 13:23:53 -06:00
vicrodh d9d5cd0771 fix(qt): playlist row removal and remote-source cover art
- Remove from playlist did nothing for a Qobuz row. The membership-id split
  made a row's display id (the catalog id QML sends) differ from its
  playlist_track_id (the membership id), but remove_track still matched the
  open document and called playlist/deleteTracks by the catalog id, so the
  lookup found nothing and the endpoint got a non-membership id. Resolve the
  catalog id to the row's membership id, remove exactly that row, and delete by
  membership; same-catalog duplicates keep their other copy. The local and
  mixed-sidecar arms were already correct. Verified by smoke test. (#764)
- Media-server rows (Jellyfin, Subsonic/Navidrome and Plex) showed no cover in
  the playlist TrackRow while the queue and NowPlayingBar did. The row fed
  cached_path the raw artwork token, which classify cannot resolve, whereas the
  queue namespaces it through portable_artwork_ref. Build the row cover from
  that same ref, and warm uncached covers on the local-playlist load (the Qobuz
  loader already did) so the cover appears without playing the track.

cc
2026-09-10 11:22:08 -06:00
vicrodh 04f6ef44e4 feat(qt): 2.1.x UI polish round — header selects, seekbar, shortcuts, per-tab filters
- Homologate the three Library header selects to the ambient Group style; Sort
  gains an arrow-down-up glyph, a "Sort" placeholder and an explicit Default,
  and every select's border clears when it returns to its default value.
- Seekbar fluidity: a drag scrubs locally and commits a single seek on release;
  the time readout is a floating opaque tooltip reparented to the overlay so the
  content-frame shadow no longer bleeds noise over it.
- Route the NowPlayingBar info button and title to the LOCAL track-info modal
  for a local row, so a local track no longer opens an empty Qobuz modal.
- App menu shows each entry's shortcut (muted, smaller, right-aligned, resolved
  per platform — Cmd on macOS, Ctrl elsewhere) and gains a Quit entry; the
  keyboard-shortcuts modal adds Close Window and Quit under Navigation, moves
  Immersive below Interface, and tightens row heights. i18n for Quit/Close
  Window across all eight locales.
- Persist each Library tab's filter toggles to disk, independently per tab.

cc
2026-09-10 11:21:50 -06:00
vicrodh 2181b379bb merge: restore macOS Exclusive mode row and cascade (#748)
cc
2026-09-09 22:44:16 -06:00
vicrodh e58ec85eb2 fix(qt): restore the Exclusive mode row on macOS (#748)
System default on macOS is the CoreAudio exclusive backend: with
`exclusive_mode` set, qbz-audio takes Hog Mode, pins the device and switches
its nominal rate to the track (PR #391, shipped in 1.2.11 and never removed).
The Tauri settings gated the toggle on `ALSA || (macos && SystemDefault)`;
the Slint port copied only the ALSA half, and fc0d35e29 then hid the row on
macOS on the premise that no backend there could honour it. So 2.0.x showed
a grey row and 2.1.0 none, while the DB flag still engaged Hog Mode.

- SettingsDoc publishes `backendIsCoreAudio` (macOS && System default); the
  row is visible and enabled on it. Linux and Windows are unchanged.
- The backend-switch cascade goes through one predicate
  (`output_labels::backend_honours_exclusive`), so re-selecting Auto or
  System default on macOS no longer clears the flag.
- The output LED reads EXCL on macOS System default + exclusive_mode instead
  of DEFAULT while the device is hogged.
- Node test `scripts/test_qt_exclusive_gate.mjs` evaluates the row's real
  gate expressions across platforms; added to qt-runtime-gate.sh. Rust tests
  cover the predicate, the LED and the serde name QML reads.

Verified by smoke on the Mac mini with a USB DAC: the row appears, Hog Mode
is acquired, and browser audio moves to the built-in speakers while QBZ
plays. qbz-qt suite 782/782 on Linux; 13/13 with cfg(macos) on the Mac.

cc
2026-09-09 22:44:16 -06:00
vicrodh 2fc03d325d fix(qt): restore window lifecycle and improve grid artwork
Restore tray-aware close and quit confirmation, circular label covers, and missing label artist portraits. Publish artwork progressively, preload two grid rows, and skip redundant motion reports while preserving data refreshes.

Include regression tests and isolated artwork benchmarks. Verified Linux build, QML checks, and scrolling smoke tests.

cx
2026-09-09 21:58:25 -06:00
vicrodh 75b8eee1c6 feat(media): add Jellyfin Quick Connect and harden remote connections
Add cancellable Quick Connect pairing and shared connection guards for Jellyfin and Subsonic. Bound connection attempts, validate library access before saving credentials, and report per-provider connection and synchronization status.

cx
2026-09-09 17:12:45 -06:00
vicrodh 17e08e93de fix(library): restore offline visibility and unify filters and sorting
Recover missing offline library entries, recognize cached-only libraries, and refresh availability after cache changes. Unify per-tab filters, compact the toolbar, and add reversible sort criteria with complete available purchase metadata.

cx
2026-09-09 17:08:33 -06:00
vicrodh 55ddc2c768 fix(qt): make mouse wheel scrolling predictable
Accumulate wheel distance with short easing instead of kinetic acceleration. Preserve rapid and fractional wheel input, immediate direction changes, nested scroll chaining and scroll-memory takeover.

Validated with 38 Qt Quick tests and desktop smoke testing.

cx
2026-09-08 22:19:32 -06:00
vicrodh 0e76c1f486 fix(qt): preserve navigation shortcuts and release text input focus
Keep search, settings and link shortcuts usable while editing, dismiss focus on outside clicks and navigation, and guard deferred focus against hidden inputs. Add Qt Quick regression coverage.

Refs #753

cx
2026-09-08 21:40:28 -06:00
vicrodh 0b6b111c74 fix(qt): correct playback commands and library browsing
Handle explicit MPRIS Play/Pause without toggling paused playback, remove the 10,000-favorite cutoff, and sort albums by original release dates. Isolate Library filters by tab, include purchased release dates, and support server-ranked artist popularity with safe pagination.

Validated with 766 Qt tests, QML checks, release-sort regressions, and a release build. Refs #750, #751.

cx
2026-09-08 18:27:51 -06:00
vicrodh 0ebcab6d3e fix(library): support SACD artwork fallback and image sidecars
Refresh missing artwork for unchanged SACD images when a folder cover is added after scanning. Enable image-specific metadata and cover sidecars in the Qt editor while preserving physical track numbers and never writing the ISO.

Validated with SACD regressions, the real Heathen image, 13 Qt editor tests, QML audits and the combined release build.

cx
2026-09-08 15:26:03 -06:00
vicrodh e4ad325744 fix(qt): unique id for the kiosk history queue delegate
The Up Next and History delegates in KioskNowPlaying both declared
`id: queueRow`. qmlcachegen and the runtime tolerate the collision, but
qmllint fails to resolve the inline QueueRowLite type in the second delegate
and the pre-release qt gate rejected it. Give the history delegate its own id.

cc
2026-09-07 12:46:58 -06:00
vicrodh 31da9c57bb fix(qt): kiosk feedback round — chrome, search, album, artists, lyrics
Address the owner's kiosk feedback on top of the release hardening:

- Chrome: the back bar is a drag surface and shrank to 44px with one shared
  button form for Back/Forward/Connect/Cast; Qobuz Connect and Cast are a
  permanent cluster in the bar; the dead Share-logs button left the kiosk
  Settings header.
- Transport bar: the hidden overflow menu is gone; Volume is always visible
  with a vertical sheet, and Desktop mode has its own button.
- Now Playing: one centred transport row (New-bar order); lyrics are centred
  (shared LyricsLinesView gains an opt-in centered, desktop default false).
- Local Library: the media funnel and an Open (ephemeral) entry point reach
  the kiosk; Artists is its own multi-column round-card grid (kiosk-only, the
  desktop LocalArtistsTab is untouched); the Artists drill-down query no
  longer hangs off a stale derived guard.
- Search: the All-tab dashboard shelves render again — they were mounted in a
  Loader Component that read the delegate id under reuse and bound an empty
  model; they are direct children now.
- Album detail: icon actions the height of the quality badge plus play next /
  later / queue, an inline floating quality badge, and zebra rows; both album
  headers show a quality badge.
- Immersive: a quiet auto-hiding kiosk exit moved to the bottom, clear of the
  window controls.

Regression coverage in scripts/test_qt_kiosk_feedback.mjs (wired into the
runtime gate). Verified on a private authenticated Xvfb; QML audits 6/6,
Node gate 5/5, release build 0 warnings.

cc
2026-09-07 12:33:28 -06:00
vicrodh b1dba198c9 feat(qt): kiosk profile release hardening
Integrate the Kiosk profile hardening round: 12 dedicated Kiosk views plus
22 shared opt-in hosts, shared navigation history with Back/Forward, Albums
as the default library tab, native paged Local Library (artists/tracks),
lightweight MyQBZ grid and detail, Ephemeral lifecycle, bounded lists and
artwork sized to the physical frame, static skeletons, 64px transport bar,
Settings touch reflow and a persistent Immersive exit.

Desktop keeps its prior behavior: every shared component defaults to the
desktop path and only opts into Kiosk behavior through explicit properties
(origin-relative ScrollMemory, viewport-bounded artwork requests).

Regression coverage: kiosk navigation and artwork Node tests added to the
runtime gate, scroll-origin fixtures for fixed and grouped rows, i18n
strings for all eight locales.

Verified by 750 release tests, six QML audits, Kiosk smokes at 800x480 and
1280x800 and a Desktop A/B with seven pixel-identical captures.

cc
2026-09-07 08:23:11 -06:00
vicrodh 0ffcdd1425 fix(discover): link recently played album artists by source
Retain artist IDs in new history snapshots and enable artist links in the carousel and View all. Route local sources to Local Library Artists and resolve older Qobuz snapshots through the existing album lookup.

cx
2026-09-06 14:34:53 -06:00
vicrodh f9578d042c fix(navigation): include local library tabs in history
Record the outgoing tab state and scroll before switching, restore same-view history entries, and route sidebar tab requests through the same path without duplicating the initial landing. Add shared CI and local regressions for source filtering and tab transitions.

cx
2026-09-06 14:34:41 -06:00
vicrodh bf84f2e65b fix(local): apply source filters to selected artist albums
Filter physical album variants in the compatibility detail view and cancel stale native queries and unfiltered retries. Load album rows when switching out of the native model.

cx
2026-09-06 14:34:18 -06:00
vicrodh 9bcc5043f3 fix(ui): keep local album source badges visible across surfaces
Carry the physical source list through Discover pins and Library feeds, including logical albums with multiple origins. Reuse the Local Library badge vocabulary and shared AlbumCard rendering so local origins remain visible in Discover, Library and MyQBZ.

Validated with 738 Qt tests and the six QML audits.

cx
2026-09-06 06:37:38 -06:00
vicrodh df8e1c89c1 fix(library): preserve explorer column choices across restarts
Store browser searches and facet selections per profile with atomic writes. Prefer navigation history when available and restore saved choices before QML can publish construction defaults. Keep the startup crash guard and unrelated preferences intact.

Validate with six QML audits and two passing profile-isolation and persistence regressions.

cx
2026-09-06 06:06:41 -06:00
vicrodh 316a0c8f46 fix(qt): replace unavailable library favorites
cx
2026-09-05 17:31:17 -06:00
vicrodh 93ba7584b0 fix(qt): polish purchases queue and search loading
cx
2026-09-05 14:39:07 -06:00
vicrodh f01950571d fix(purchases): restore local copy and download semantics
Restore downward stream-format downloads while keeping purchased masters on the download grant. Make Purchases playback prefer the highest complete local copy, repair duplicate copy inventory, and move copy management into a bounded modal with safe library and record actions.

cx
2026-09-05 12:10:55 -06:00
vicrodh 4bb4982312 feat(sacd): support raw and DST-compressed ISO playback
Parse resilient SACD images, decode DST, scan ISO releases into the local library, and keep explicit opens ephemeral.

Ship decoder notices across distribution formats and align DSD album filtering and version selection.

cx
2026-09-05 10:00:24 -06:00
vicrodh 4ac6b4006e feat(integrations): make the qobuzapp:// handler a setting
Claiming the official client's `qobuzapp://` scheme was a side effect
of installing. On Linux that displaces nobody; on macOS and Windows the
official client registers the same scheme, and on Windows — where QBZ
is only starting to be adopted — taking the browser's links over at
install time is not a reasonable default. `qbz://` (our own scheme)
stays registered unconditionally.

Settings > Integrations > "Open Qobuz links in QBZ" (link_handler_qt,
pref `qobuz_links_enabled` beside the other Integrations opt-ins),
default on for Linux/macOS and OFF on Windows. The registration moves
only when the toggle is flipped; the one startup reconciliation is
conservative — a claim pointing at this executable while the setting is
off (the 2.0.x MSI wrote one) is dropped, nothing is ever claimed on its
own.

Windows writes/removes HKCU\Software\Classes\qobuzapp (deleting only a
tree whose command names this exe); macOS uses Launch Services and hands
the default back to the first other handler on off; Linux runs xdg-mime
on. The MSI no longer registers qobuzapp and the workflow's protocol
checks cover qbz only. The Windows and macOS arms build on their
platform gates, not on this box.

cc
2026-09-04 13:10:42 -06:00
vicrodh 6d07dfdf4d feat(artist): sort for the in-library albums and a split play button on every album section
The "In library" Albums section gains the release buckets' sort select
(same five keys, persisted by artist_prefs under the pseudo release type
"library"; rows sorted in QML off the document's seed). Every album
section — each catalog bucket and the library grid — gains a split
button right of its sort (controls/QbzSplitButton.qml, sized like the
sm select): the body plays the whole section in its sort order as one
queue replacing the current one (`QbzPlayer.playAlbums` ->
`playback_qt::play_albums`, each album resolved through the single-album
path, failures skipped), the chevron offers Play random (one album of
the section) and Play selected, which arms a per-section select mode:
the cards show their check bottom-right inside the art
(`AlbumCard.selectMarkBottom`), the button reads "Play selected (n)" and
plays the checked albums in sort order, and the menu swaps that row for
Cancel selection. One section is armed at a time; changing artist or
tab disarms. No queue options on purpose.

cc
2026-09-04 13:10:42 -06:00
vicrodh c85fbac4c7 fix(artist): keep the jump-nav mounted in library mode and park the filters past the magnifier
With both source switches on over an artist whose purchases are not
hearted, the library tab's section set was empty, the strip unmounted,
and the switches — which live in the strip — went with it, leaving no
way to reset the filter. "About" (the strip's back-to-top) is now always
present in library mode, landing on the page top since the library
column carries no bio anchor; the active tab on a mode switch is still
the first real section.

The trailing slot moves to the far right of the bar, past the search
toggle: the field grows LEFT over the tabs when it opens, so controls
left of the magnifier were covered while typing.

cc
2026-09-04 11:51:59 -06:00
vicrodh 89b621264e fix(artist): in-library tab queues its own list, gets a Tracks section and a mode-aware jump-nav (#737)
A track clicked under "In library" played the artist's POPULAR queue:
every row of the page went through `QbzPlayer.playArtistTrack`, whose
queue is TOP_QUEUE and which starts that list at row 0 for any id it
does not hold — the exact symptom in #737, including "it works only when
the song is also popular". The library rows now play through
`QbzLibrary.libraryPlayVisible` with the tab's full filtered list, the
source-aware seam LibraryView uses; `PopularTrackRow` takes a
`playHandler` instead of knowing which tab it is in.

The tab itself becomes a first-class view over `artist.libraryItems`:

- Tracks section modelled on Popular Tracks — header action cluster
  (Play all, select mode, the shared ⋯ menu re-pointed at the library
  list), 10-row preview built from a numeric model, Load more / View
  less opening the whole list.
- JUMP TO follows the mode: in library only the Tracks / Albums sections
  that have rows, resolving anchors against the library column (the
  catalog set stayed up before and "Popular Tracks" scrolled to a section
  that was not drawn).
- Purchases / favourites switches float in the strip while the tab is
  active, with the Library > Albums/Tracks semantics (neither = union,
  one = that source, both = intersection) and page-local state.
- A cleared heart fades the row (purchases stay); the settle republishes
  the document and the row leaves. When the last row goes the page
  returns to the catalog instead of an empty tab.
- Selection and bulk actions run over the active mode's lists; a mode
  switch clears the selection and re-arms the warm from cache.

The view no longer parses `QbzLibrary.libraryJson` per binding.

cc
2026-09-04 10:41:52 -06:00
vicrodh 010b1da8d8 feat(qt): warmLibrary invokable and a trailing slot on the jump-nav bar
`QbzArtist.warmLibrary` lets the artist page's "In library" toggle re-arm
the Library feed warm (a fetch only if the session never loaded it, a
cache repaint otherwise). `QbzJumpNavBar.trailing` is a host-filled slot
between the tab strip and the magnifier, sized by its children and
costing nothing when empty, so a page can float mode-specific controls
in the strip without reflowing the tabs.

cc
2026-09-04 10:41:51 -06:00
vicrodh 30912962d3 feat(library): refine track source controls
Preserve the remaining queue when shuffle is disabled, expose clear transport state tooltips, retain Library source filters for the session, and add independent track-artwork preferences for Queue and Library.

cx
2026-09-03 14:53:22 -06:00
vicrodh fb928df2ff fix(library): retain source filters for session
cx
2026-09-03 09:07:22 -06:00
vicrodh 23941a0958 feat(purchases): refine purchased album presentation
cx
2026-09-03 09:07:18 -06:00
vicrodh c0a37e6b87 feat(purchases): manage independent download copies
cx
2026-09-03 05:13:05 -06:00
vicrodh 561b3c25c7 feat(purchases): gate album actions by entitlement
cx
2026-09-03 04:32:23 -06:00
vicrodh 8aab112afe feat(dsd): support seeking in direct playback
Add random access for DSF, DFF, and SACD demuxers, replace live DoP/native sources without releasing ALSA, and expose seek through Qt, MPRIS, and the daemon API.

cx
2026-09-03 02:32:52 -06:00
vicrodh ea92f07b66 fix(local-library): restore responsive source-aware browsing
Show SACD images in folder grouping, keep track details responsive, avoid repeated album reload work, and exclude disabled or unconfigured media servers from filters and sync.

cx
2026-09-03 02:32:37 -06:00
vicrodh 1f54fd9fcb fix(qt): Local Library header gear opens Settings > Local Library
It landed on the Audio root; the row now sets section 4 through the same
invokable the settings sub-navigation uses before navigating.

cc
2026-09-02 20:34:59 -06:00
vicrodh baf0dc2bb9 feat(local-library): DSD quality chip
DSD gets its own Quality chip and Hi-Res goes back to 24-bit PCM only, in
the MediaFilter, the native catalog SQL (tracks CASE and the materialized
album quality_tier) and the legacy QML filters. A Format chip would be the
same predicate, so there is none. Schema version 6 re-derives the album
tiers that were materialized as hires.

cc
2026-09-02 20:34:59 -06:00
vicrodh 35ff1818f6 feat(qt): say "not yet available" on pre-release tracks
/album/get sends streamable_at and release_date_stream per track: a future
value marks a pre-release, null a pulled track. Track carries both fields
and Track::is_upcoming applies the one rule (future streamable_at wins,
then a future release date); the album, playlist, search and label rows
publish qobuzUpcoming and the unavailable glyph reads "This track is not
yet available" instead of "no longer". Artist page rows carry no dates and
keep the generic copy. New msgid in the seven translated catalogs.

cc
2026-09-02 20:34:58 -06:00
vicrodh feb2c9de12 fix(qt): pin the quality stamp tooltip to its text
The now-playing stamp bubble came out wider than its lines with the text
offset, and never reproduced in isolation. The three stamp ToolTips now
bind width/height straight to the content's implicit size with zero
paddings, insets and margins, leaving nothing for the style to add.

cc
2026-09-02 20:34:58 -06:00
vicrodh bef2a8847a fix(discover): honor blacklist and prune stale pins
cx
2026-09-02 16:03:26 -06:00
vicrodh 472706f84e fix(settings): harden migration and accessibility
Apply interface scaling through Qt at startup, complete keyboard navigation, move account migration into setup and progress modals, and defer Subsonic sync while the server scans.

cx
2026-09-02 14:33:08 -06:00
vicrodh 015ca48994 merge: integrate ListenBrainz connect-state fix
cx
2026-09-02 10:28:57 -06:00
vicrodh c8ad6a5215 fix(settings): serialize ListenBrainz token validation
cx
2026-09-02 10:28:41 -06:00
vicrodh a39ea8c12d feat(settings): Import / Export section with portable blacklist and account migration
A new top-level Settings section, Import / Export, gathers everything that
moves data in or out of this install:

- SETTINGS PORTABILITY: the settings bundle export moves here from
  Developer, with a note on what the bundle does not hold (favorites,
  playlists, blacklist, purchases).
- BLACKLIST: export the blocked artists and albums as a JSON one user can
  hand to another; import merges additively and never touches the enabled
  flag (qbz_app::settings::blacklist_portable, frontend-agnostic).
- ACCOUNT MIGRATION: create a snapshot while signed in with the old
  account, then migrate it into the account signed in now, with the local
  profile copy and its three opt-in toggles (media servers, scrobblers,
  listening history, all on). The disclaimer states that subscriptions and
  purchases do not move. Progress and results are status lines in the
  settings document; the favorites cache re-warms and the sidebar reloads
  after a run, the local half asks for a restart.

Also: the header shows a translated "Member" for accounts without a
subscription, and the login screen says what a member can and cannot do.
50 new strings in all eight catalogs.

cc
2026-09-02 08:24:02 -06:00
vicrodh a5376e9064 feat(shell): active purchase downloads live in the sidebar, as a ring when compact
The Downloads section leaves the nav flyout. With the sidebar open, the
purchases downloading now sit under the Purchases entry, one row per
album with a thin progress bar, each reopening its detail. With the
sidebar collapsed, and in the compact header nav when it is closed, they
fold into one progress ring whose hover bubble lists every album with
its count and percentage. The ring is a Canvas that repaints only when a
publish moves the value — no timer, per the repaint-pulse rule. The
active set now carries done/total/percent per album.

cc
2026-09-02 07:11:03 -06:00
vicrodh db1d36537b feat(purchases): genre filter, zebra rows, and a list that no longer overlaps
The list view's quality column was 110 px for a badge that is 136 px
wide, so the badge ran into the release-date column; it is 150 now, the
album list's width. Both list rows and track rows draw the odd-row
zebra every other list in the app draws. The filter panel gains a Genre
section built from the catalog genre on each purchased album (albums
tab only, transient like group and sort), counted and summarised with
the other filters.

cc
2026-09-02 07:11:03 -06:00
vicrodh 29a6528ec1 feat(quality): DSD wears its mark in the Local Library and MyQBZ too
The local rows, the source-side quality hint and the MyQBZ badge still
tiered a 1-bit file as "hires"; they now say `dsd`, so the same DSD mark
the purchases show reaches every local surface, and the Local Library's
hi-res filter keeps counting DSD as hi-res.

cc
2026-09-02 07:11:03 -06:00
vicrodh 07e683f5ef feat(purchases): detail polish and a Downloads section in the nav flyout
The purchase detail badges what was bought on every row (DSD128 on a
DSD purchase, not the catalog's 24/352.8), marks the formats already on
disk with a check in the dropdown, offers a track filter, links to the
album in the Local Library once its folder is indexed, and gives the
download the accent while Play drops to a plain control of the same
size — on this screen the download is the point.

Downloads are process-wide and survive leaving the page; the nav flyout
now lists the purchases with a download in flight under "Downloads · N"
so the user can find them again, each row reopening its detail.

cc
2026-09-02 01:30:12 -06:00