Files
minstrel/flutter_client/lib/library/library_providers.dart
T
bvandeusen 947d4401a6 feat(flutter/cache): migrate artistProvider to drift-first (#357 plan C)
artistProvider becomes a StreamProvider backed by drift's cached_artists
watch(). Empty + online triggers REST fallback; populates drift; watch
re-emits. Empty + offline yields a sentinel ArtistRef(id: '', name: '');
surfaces should already check state.value?.id.isNotEmpty.

AsyncValue<ArtistRef> shape unchanged so .when callsites keep working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:18:59 -04:00

84 lines
3.1 KiB
Dart

import 'package:dio/dio.dart';
import 'package:drift/drift.dart' as drift;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../api/client.dart';
import '../api/endpoints/library.dart';
import '../auth/auth_provider.dart';
import '../cache/adapters.dart';
import '../cache/audio_cache_manager.dart' show appDbProvider;
import '../cache/cache_first.dart';
import '../cache/connectivity_provider.dart';
import '../cache/db.dart';
import '../models/album.dart';
import '../models/artist.dart';
import '../models/home_data.dart';
import '../models/track.dart';
/// Shared authenticated dio. This is the ONLY place tokenResolver is wired
/// to the secure-storage `session_token` read — every other endpoint
/// awaits `dioProvider.future` rather than constructing its own dio.
///
/// Riverpod will invalidate any FutureProvider that watches this when
/// the server URL changes; auth state changes don't need to invalidate
/// the provider because the resolver re-reads the token on every request.
final dioProvider = FutureProvider<Dio>((ref) async {
final url = await ref.watch(serverUrlProvider.future);
if (url == null || url.isEmpty) {
throw StateError('no server URL set');
}
final storage = ref.watch(secureStorageProvider);
return ApiClient.buildDio(
baseUrl: url,
tokenResolver: () async => storage.read(key: 'session_token'),
on401: () async => ref.read(authControllerProvider.notifier).clearSession(),
);
});
final libraryApiProvider = FutureProvider<LibraryApi>((ref) async {
return LibraryApi(await ref.watch(dioProvider.future));
});
final homeProvider = FutureProvider<HomeData>((ref) async {
return (await ref.watch(libraryApiProvider.future)).getHome();
});
/// Drift-first per #357 plan C. Watches cached_artists for the row;
/// when empty + online, fetches via REST + populates drift, which
/// re-emits via watch().
final artistProvider =
StreamProvider.family<ArtistRef, String>((ref, id) {
final db = ref.watch(appDbProvider);
return cacheFirst<CachedArtist, ArtistRef>(
driftStream: (db.select(db.cachedArtists)..where((t) => t.id.equals(id)))
.watch(),
fetchAndPopulate: () async {
final api = await ref.read(libraryApiProvider.future);
final fresh = await api.getArtist(id);
await db.into(db.cachedArtists).insertOnConflictUpdate(fresh.toDrift());
},
toResult: (rows) => rows.isEmpty
? const ArtistRef(id: '', name: '')
: rows.first.toRef(),
isOnline: () async =>
(await ref.read(connectivityProvider.future)),
);
});
final artistAlbumsProvider =
FutureProvider.family<List<AlbumRef>, String>((ref, id) async {
return (await ref.watch(libraryApiProvider.future)).getArtistAlbums(id);
});
final artistTracksProvider =
FutureProvider.family<List<TrackRef>, String>((ref, id) async {
return (await ref.watch(libraryApiProvider.future)).getArtistTracks(id);
});
final albumProvider =
FutureProvider.family<({AlbumRef album, List<TrackRef> tracks}), String>(
(ref, id) async {
return (await ref.watch(libraryApiProvider.future)).getAlbum(id);
},
);