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 cached playlists filtered to /// match the `kind` family arg: /// - 'user' → only user-created (systemVariant null), owned by current user /// - 'system' → only system-generated (systemVariant non-null), owned /// - 'all' → everything: own user playlists + own system + others' public /// /// systemVariant column added in drift schema v2 (#357 follow-up); previous /// behavior leaked system playlists into add-to-playlist sheet because we /// couldn't filter locally. 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 filtered = rows.where((r) { if (kind == 'user') return r.systemVariant == null; if (kind == 'system') return r.systemVariant != null; return true; // 'all' }); final owned = filtered .where((r) => r.userId == user.id) .map((r) => r.toRef()) .toList(); final pub = filtered .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(); });