import 'package:drift/drift.dart' as drift; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../api/endpoints/me.dart'; import '../api/endpoints/playlists.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 '../library/library_providers.dart' show dioProvider; import '../models/playlist.dart'; import '../models/system_playlists_status.dart'; final playlistsApiProvider = FutureProvider((ref) async { return PlaylistsApi(await ref.watch(dioProvider.future)); }); /// Drift-first per #357 plan C. Returns all cached playlists for the /// current user (owned) + public playlists from others. The `kind` /// family arg only affects the REST cold-cache fetch shape — drift /// can't distinguish 'user' from 'system' kind because systemVariant /// isn't persisted (TODO: add column + schema bump in a follow-up). /// For v1 this means add-to-playlist sheet shows system playlists too. final playlistsListProvider = StreamProvider.family((ref, kind) { final db = ref.watch(appDbProvider); final user = ref.watch(authControllerProvider).value; return cacheFirst( driftStream: db.select(db.cachedPlaylists).watch(), fetchAndPopulate: () async { final api = await ref.read(playlistsApiProvider.future); final fresh = await api.list(kind: kind); await db.batch((b) { for (final p in fresh.all) { b.insert(db.cachedPlaylists, p.toDrift(), mode: drift.InsertMode.insertOrReplace); } }); }, toResult: (rows) { if (user == null) return PlaylistsList.empty(); final owned = rows .where((r) => r.userId == user.id) .map((r) => r.toRef()) .toList(); final pub = rows .where((r) => r.userId != user.id && r.isPublic) .map((r) => r.toRef()) .toList(); return PlaylistsList(owned: owned, public: pub); }, isOnline: () async => (await ref.read(connectivityProvider.future)), ); }); /// Composite shape (playlist + tracks). async* over the playlist watch /// stream + a one-shot tracks fetch per emission. final playlistDetailProvider = StreamProvider.family((ref, id) async* { final db = ref.watch(appDbProvider); final user = ref.watch(authControllerProvider).value; final playlistQuery = db.select(db.cachedPlaylists) ..where((t) => t.id.equals(id)); final tracksQuery = (db.select(db.cachedPlaylistTracks) ..where((t) => t.playlistId.equals(id)) ..orderBy([(t) => drift.OrderingTerm.asc(t.position)])) .join([ drift.leftOuterJoin(db.cachedTracks, db.cachedTracks.id.equalsExp(db.cachedPlaylistTracks.trackId)), drift.leftOuterJoin(db.cachedArtists, db.cachedArtists.id.equalsExp(db.cachedTracks.artistId)), drift.leftOuterJoin(db.cachedAlbums, db.cachedAlbums.id.equalsExp(db.cachedTracks.albumId)), ]); PlaylistDetail emptyDetail() => PlaylistDetail( playlist: Playlist( id: id, userId: user?.id ?? '', name: '', description: '', isPublic: false, systemVariant: null, trackCount: 0, coverUrl: '', ownerUsername: '', createdAt: '', updatedAt: '', ), tracks: const [], ); await for (final playlistRows in playlistQuery.watch()) { if (playlistRows.isEmpty) { if (await ref.read(connectivityProvider.future)) { try { final api = await ref.read(playlistsApiProvider.future); final fresh = await api.get(id); await db.batch((b) { b.insert(db.cachedPlaylists, fresh.playlist.toDrift(), mode: drift.InsertMode.insertOrReplace); for (var i = 0; i < fresh.tracks.length; i++) { final t = fresh.tracks[i]; if (t.trackId == null) continue; b.insert( db.cachedPlaylistTracks, CachedPlaylistTracksCompanion.insert( playlistId: id, trackId: t.trackId!, position: drift.Value(i), ), mode: drift.InsertMode.insertOrReplace, ); } }); // watch() re-emits next iteration with populated row. } catch (_) { yield emptyDetail(); } } else { yield emptyDetail(); } continue; } final playlist = playlistRows.first.toRef(); final trackRows = await tracksQuery.get(); final tracks = trackRows.asMap().entries.map((e) { final r = e.value; final track = r.readTableOrNull(db.cachedTracks); final artist = r.readTableOrNull(db.cachedArtists); final album = r.readTableOrNull(db.cachedAlbums); return PlaylistTrack( position: e.key, trackId: track?.id, title: track?.title ?? '', albumId: album?.id, albumTitle: album?.title ?? '', artistId: artist?.id, artistName: artist?.name ?? '', durationSec: track == null ? 0 : track.durationMs ~/ 1000, streamUrl: null, ); }).toList(); yield PlaylistDetail(playlist: playlist, tracks: tracks); } }); final systemPlaylistsStatusProvider = FutureProvider((ref) async { final dio = await ref.watch(dioProvider.future); return MeApi(dio).systemPlaylistsStatus(); });