artistAlbumsProvider populates cachedAlbums (album rows only) without
ever fetching their tracks. When the user taps from the artist grid
into an album, albumProvider sees a drift hit on the album row, never
triggers cold-cache, and yields with an empty track list — visible as
the album header rendering above an empty body.
Refactor albumProvider's cold-cache logic into a helper closure that
either branch (no album row OR album row + no tracks) can call. Add a
fetchAttempted guard so a server response that legitimately returns
zero tracks doesn't loop forever.
Decision flow:
- albumRows empty: cold-fetch (existing path), yield empty on
failure/offline
- albumRows hit but trackRows empty AND not yet attempted: same
cold-fetch — drift watch re-emits with the populated tracks
- albumRows hit, trackRows hit (or attempted already): yield as-is
Track row (album detail):
- Vertical padding 10→6 and only render the artist line when non-empty,
so tracks without a populated artist row don't reserve a blank line.
- Number column 28→22 — visually tighter without making 3-digit
numbers cramp.
Mini player missing artist:
- Symptom was an empty `artist` field on MediaItem after tapping a
track in album detail; layout change wasn't the cause. Real cause:
albumProvider's cold-cache only wrote cachedAlbums + cachedTracks,
never cachedArtists. The drift JOINs that build the AlbumRef + each
TrackRef returned null for the artist row, so artistName fell back
to '' and the audio handler stamped MediaItem.artist=''.
- Cold-cache now collects every distinct (artistId, artistName) pair
off the album header + each track, inserts them into cachedArtists
alongside the album/tracks. Subsequent JOINs populate artistName,
the audio handler stamps it on MediaItem, and the mini player picks
it up via the existing artistName.isNotEmpty render.
- Existing cached albums won't get artist names until they're
re-fetched (cache invalidation TBD). Future cold-cache hits will.
Artist detail album grid:
- AlbumCard gains showArtist (default true). Pass false in the artist
detail grid since the page header already names the artist. cellH
trimmed by the 16dp artist line.
Album detail screen rendered the cover slot as a bare slate Container
— no actual image. Wire ServerImage at /api/albums/<id>/cover (96dp,
rounded). Same for artist detail: ClipOval around ServerImage of the
artist's coverUrl, falling back to slate when empty.
Artist navigation hang: artistProvider goes through cacheFirst, which
had no logging — so we couldn't see whether the cold-cache fetch was
running. Add an optional `tag` parameter that, when set, debugPrints
each step (drift hit/miss, connectivity, fetch start/done/error).
Wire tags into artistProvider and artistAlbumsProvider, plus add the
3s connectivity timeout there too as belt-and-suspenders.
Next test pass should show cacheFirst[artist(<id>)]: lines that
pinpoint where the artist screen is sticking.
Two changes to surface where detail-screen spinners hang:
1. Connectivity().checkConnectivity() goes over a platform channel
that on some Android builds stalls. Wrap with a 2s timeout and
default to "online" if it times out — being wrong about
connectivity costs at most one failed fetch, but being stuck
spins the UI forever.
2. albumProvider's cold-cache flow now debugPrints at every step:
drift miss → connectivity result → API call → drift write → ack
the re-emit. Also adds a 10s timeout on the API fetch and a 3s
timeout on the connectivity await so a hang surfaces as an
error-yielded empty AlbumRef instead of an infinite spinner.
Once the operator's next test produces logs we can pinpoint which
step is hanging. The diagnostic prints stay until the issue's root
cause is confirmed; will be removed in a follow-up.
- cache_first.dart: backtick-fence List<T> doc comment to dodge
unintended_html_in_doc_comment.
- library_providers.dart:163: switch single-row insert to
insertAllOnConflictUpdate; Batch only exposes the *AllOnConflict*
variant.
- 5 test files: StreamProvider.overrideWith takes Create<Stream<T>>,
not Create<Future<T>>. Wrap data emissions in Stream.value(...).
- artist_detail_screen_test: add models/album.dart import for AlbumRef
type annotation.
- track_actions_sheet_test: drop _StubLiked extends LikedIdsController
(notifier no longer exists post-migration); override with
Stream.value(LikedIds(...)) instead.
- like_button_test: rollback assertion now requires drift writes via
LikesController. Skip under _skipDrift until libsqlite3-dev lands on
the runner image (Fable #399). Replace stale .notifier reference
with likesControllerProvider for completeness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three providers all become StreamProviders driven by drift watch():
- artistAlbumsProvider: cacheFirst over the join of cached_albums +
cached_artists for artist_name on each row.
- artistTracksProvider: cacheFirst over the join of cached_tracks +
cached_artists + cached_albums for snapshot fields.
- albumProvider: composite ({album, tracks}) shape, so uses async*
with two queries (album + tracks) for cleaner reactive behavior.
Cold-cache fallback inlined; populates both album and tracks tables
in one batch.
.future call site in artist_detail_screen.dart left as-is —
StreamProvider.future returns the next emission with the same
signature, so no consumer change needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ApiClient.buildDio takes on401; the library's dioProvider wires it to
the auth controller's clearSession. The router redirect already handles
navigation away from authed screens once the session goes null.
HomeScreen surfaces the banner on connection_refused with Retry +
Change URL.
LibraryApi wraps GET /api/home, /api/artists/{id}(/tracks),
/api/albums/{id}. dioProvider builds an authenticated dio (token
resolver reads session_token from secure storage on every request).
homeProvider, artistProvider(id), albumProvider(id) sit on top.