import 'package:audio_service/audio_service.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../api/endpoints/radio.dart'; import '../auth/auth_provider.dart'; import '../cache/audio_cache_manager.dart'; import '../library/library_providers.dart' show dioProvider; import '../models/track.dart'; import 'album_cover_cache.dart'; import 'audio_handler.dart'; final audioHandlerProvider = Provider((ref) { throw UnimplementedError('overridden in main()'); }); final albumCoverCacheProvider = Provider((ref) { return AlbumCoverCache( dioFactory: () => ref.read(dioProvider.future), ); }); final playbackStateProvider = StreamProvider( (ref) => ref.watch(audioHandlerProvider).playbackState, ); final mediaItemProvider = StreamProvider( (ref) => ref.watch(audioHandlerProvider).mediaItem, ); final queueProvider = StreamProvider>( (ref) => ref.watch(audioHandlerProvider).queue, ); /// Player volume in [0.0, 1.0]. Mirrors just_audio's player.volume; /// modify via PlayerActions.setVolume(). Surfaced separately from /// PlaybackState because audio_service's PlaybackState doesn't carry /// volume — it's a player-side concern. final volumeProvider = StreamProvider( (ref) => ref.watch(audioHandlerProvider).volumeStream, ); /// Live playback position. just_audio emits at ~200ms cadence so seek /// bars driven by this provider scrub smoothly. Don't use /// PlaybackState.updatePosition for that — it only changes on state /// transitions (play/pause/buffer/seek) and the bar would appear /// frozen between events. final positionProvider = StreamProvider( (ref) => ref.watch(audioHandlerProvider).positionStream, ); class PlayerActions { PlayerActions(this._ref); final Ref _ref; Future playTracks(List tracks, {int initialIndex = 0}) async { final url = await _ref.read(serverUrlProvider.future); final token = await _ref.read(secureStorageProvider).read(key: 'session_token'); final cache = _ref.read(albumCoverCacheProvider); final audioCache = _ref.read(audioCacheManagerProvider); final h = _ref.read(audioHandlerProvider) ..configure( baseUrl: url ?? '', token: token, coverCache: cache, audioCacheManager: audioCache, ); await h.setQueueFromTracks(tracks, initialIndex: initialIndex); await h.play(); } Future playNext(TrackRef track) async { final h = _ref.read(audioHandlerProvider); await h.playNext(track); } Future enqueue(TrackRef track) async { final h = _ref.read(audioHandlerProvider); await h.enqueue(track); } /// Toggles shuffle on/off. Pre-existing audio_service convention. Future toggleShuffle() async { final h = _ref.read(audioHandlerProvider); final state = await h.playbackState.first; final next = state.shuffleMode == AudioServiceShuffleMode.none ? AudioServiceShuffleMode.all : AudioServiceShuffleMode.none; await h.setShuffleMode(next); } /// Cycles repeat mode: none → all → one → none. Future cycleRepeat() async { final h = _ref.read(audioHandlerProvider); final state = await h.playbackState.first; final next = switch (state.repeatMode) { AudioServiceRepeatMode.none => AudioServiceRepeatMode.all, AudioServiceRepeatMode.all => AudioServiceRepeatMode.one, _ => AudioServiceRepeatMode.none, }; await h.setRepeatMode(next); } Future setVolume(double v) async { await _ref.read(audioHandlerProvider).setVolume(v); } /// Fetches `/api/radio?seed_track=` and starts playing the /// returned track list (seed at index 0 + recommended picks). Future startRadio(String trackId) async { final dio = await _ref.read(dioProvider.future); final tracks = await RadioApi(dio).seedTrack(trackId); if (tracks.isEmpty) return; await playTracks(tracks); } } final playerActionsProvider = Provider((ref) => PlayerActions(ref));