feat(flutter): home screen on per-item rendering (Slice C)

End-to-end pilot of the per-item architecture. Home now reads from
the new homeIndexProvider (drift-first over CachedHomeIndex with
/api/home/index discovery + SWR), then each tile is a small
ConsumerWidget watching its own albumTileProvider/artistTileProvider/
trackTileProvider. Tiles render a matched-dimension skeleton while
their entity is still hydrating, and swap in the real card once
drift emits the populated row.

Track hydration is wired up — /api/tracks/:id already existed so
the queue's case 'track' just calls api.getTrack(id) and persists.

The visible behavior:
* Cold visit: small /api/home/index round-trip (IDs only, ~10×
  smaller than /api/home), then sections appear shaped with
  skeleton tiles; each tile materializes as the hydration queue
  drains. No more "30s blank → everything pops in at once."
* Warm visit: drift index emits instantly, drift entity rows emit
  instantly, no network. Page paints fully in the first frame.
* Mid-state: scrolling through a partially-hydrated section sees
  real cards next to skeleton cards. Layout doesn't shift because
  skeletons match real-card dimensions exactly.

CachedHomeSnapshot (and the legacy homeProvider) stay in place but
unconsumed by Flutter — left in for now so revert is cheap if the
new path needs reworking. Cleanup follow-up in a later slice.

Old /api/home endpoint untouched, so the web client keeps working
unchanged.
This commit is contained in:
2026-05-13 21:05:43 -04:00
parent 0119eacf14
commit 03c13d21c6
7 changed files with 345 additions and 156 deletions
+8 -4
View File
@@ -78,10 +78,7 @@ class HydrationQueue {
case 'artist':
await _hydrateArtist(req.entityId);
case 'track':
// TODO(per-item slice B): GET /api/tracks/:id doesn't exist
// yet; tracks remain bulk-loaded until that endpoint lands.
// Tile providers for tracks read drift but never enqueue.
break;
await _hydrateTrack(req.entityId);
}
} catch (_) {
// Swallow — tile stays in skeleton. A retry-on-visit pass can
@@ -114,6 +111,13 @@ class HydrationQueue {
final db = _ref.read(appDbProvider);
await db.into(db.cachedArtists).insertOnConflictUpdate(fresh.toDrift());
}
Future<void> _hydrateTrack(String id) async {
final api = await _ref.read(libraryApiProvider.future);
final fresh = await api.getTrack(id);
final db = _ref.read(appDbProvider);
await db.into(db.cachedTracks).insertOnConflictUpdate(fresh.toDrift());
}
}
class _HydrationRequest {