feat(flutter): drift-first Library tabs + systemPlaylistsStatus + tab pre-warm

Four-part change to push more surfaces onto the drift cache and
eliminate cold-tab-visit latency on the Library screen.

* **Library Artists tab** — _libraryArtistsProvider migrates from
  REST-paginated AsyncNotifier with infinite-scroll loadMore to a
  drift-first StreamProvider over cached_artists ordered by
  sortName. Sync already populates the full set; cacheFirst's
  fetchAndPopulate covers the fresh-install + sync-not-yet-done
  cold case via /api/artists?limit=1000. SWR refresh on every
  visit. GridView.builder lazily realizes only visible cells so
  loading the full list up front is fine for typical libraries.
  loadMore + NotificationListener gone.

* **Library Albums tab** — same migration, drift-first over
  cached_albums joined with cached_artists for the artistName
  field.

* **systemPlaylistsStatusProvider** — new CachedSystemPlaylistsStatus
  single-row table (schema 6 → 7, JSON blob like CachedHomeSnapshot)
  for the home Playlists row's "building / pending / failed"
  placeholder logic. Drift-first means the row paints with the
  prior status instantly instead of flickering through
  SystemPlaylistsStatus.empty() while the REST call resolves.

* **Library screen tab pre-warm** — ref.listen on all 5 tab
  providers in _LibraryScreenState.build subscribes them upfront
  so swiping between tabs feels instant rather than each tab
  paying its own cold-cache cost on first visit. cacheFirst
  handles dedupe of concurrent fetchAndPopulate triggers.

Test mock updated for the StreamProvider type change on
systemPlaylistsStatusProvider.
This commit is contained in:
2026-05-14 10:14:46 -04:00
parent 507c532f6d
commit 2a18e91c39
4 changed files with 201 additions and 165 deletions
+24 -1
View File
@@ -142,6 +142,21 @@ class CachedHistorySnapshot extends Table {
Set<Column> get primaryKey => {id};
}
/// Single-row cache of the /api/me/system-playlists-status response.
/// The home Playlists row reads this every render to pick between
/// real cards and placeholder cards for For-You / Discover / Songs-
/// like slots, so a fresh-mount cold fetch produces a visible
/// "building / pending / failed" flicker. Storing the last result
/// as a JSON blob means the home renders with the prior status
/// instantly, then SWR refreshes underneath. Schema 7+.
class CachedSystemPlaylistsStatus extends Table {
IntColumn get id => integer().withDefault(const Constant(1))();
TextColumn get json => text()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
/// Section→position→entity-id index for the home screen, populated
/// from the per-item discovery endpoint `/api/home/index`. Each row
/// pins one tile slot to an entity; the actual entity data lives in
@@ -201,12 +216,13 @@ enum CacheSource { manual, autoLiked, autoPlaylist, autoPrefetch, incidental }
CachedHistorySnapshot,
CachedQuarantineMine,
CachedHomeIndex,
CachedSystemPlaylistsStatus,
])
class AppDb extends _$AppDb {
AppDb([QueryExecutor? e]) : super(e ?? driftDatabase(name: 'minstrel_cache'));
@override
int get schemaVersion => 6;
int get schemaVersion => 7;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -247,6 +263,13 @@ class AppDb extends _$AppDb {
// builds still read from it as a fallback path.
await m.createTable(cachedHomeIndex);
}
if (from < 7) {
// Schema 7: cached_system_playlists_status. Single-row
// snapshot of /api/me/system-playlists-status used by the
// home Playlists row. Empty on upgrade; first fetch
// populates.
await m.createTable(cachedSystemPlaylistsStatus);
}
},
);
}