import 'package:audio_service/audio_service.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/services/library_database.dart'; import 'package:spotiflac_android/services/music_player_service.dart'; final currentMediaItemProvider = StreamProvider((ref) { return musicPlayerMediaItemEvents(); }); final playbackStateProvider = StreamProvider((ref) { return musicPlayerPlaybackStateEvents(); }); /// Small derived providers keep position ticks from rebuilding widgets that /// only care about transport state (and vice versa). final playbackPositionProvider = Provider((ref) { return ref.watch( playbackStateProvider.select( (state) => state.value?.position ?? Duration.zero, ), ); }); final playbackPlayingProvider = Provider((ref) { return ref.watch( playbackStateProvider.select((state) => state.value?.playing ?? false), ); }); final playbackLoadingProvider = Provider((ref) { return ref.watch( playbackStateProvider.select((state) { final processingState = state.value?.processingState; return processingState == AudioProcessingState.loading || processingState == AudioProcessingState.buffering; }), ); }); /// Transport state for one media item. Non-current Library cells only watch /// the current media ID; the active cell additionally follows play/loading so /// a transport toggle does not rebuild the entire Library grid. final mediaItemPlaybackUiProvider = Provider.family<({bool isCurrent, bool isPlaying, bool isLoading}), String>( (ref, mediaId) { final currentId = ref.watch( currentMediaItemProvider.select((state) => state.value?.id), ); if (mediaId.isEmpty || currentId != mediaId) { return (isCurrent: false, isPlaying: false, isLoading: false); } return ( isCurrent: true, isPlaying: ref.watch(playbackPlayingProvider), isLoading: ref.watch(playbackLoadingProvider), ); }, ); final playQueueProvider = StreamProvider>((ref) { return musicPlayerQueueEvents(); }); class MusicPlayerController { const MusicPlayerController(); MusicPlayerHandler? get _handler => musicPlayerHandler; bool get isAvailable => _handler != null; DateTime? get sleepTimerEndsAt => _handler?.sleepTimerEndsAt; Future ensureInitialized() async { try { return await initMusicPlayer(); } catch (_) { return null; } } Future playAll( List items, { int initialIndex = 0, }) async { final handler = await ensureInitialized(); await handler?.setQueueAndPlay(items, initialIndex: initialIndex); } Future playSingle(PlayableMedia item) => playAll([item]); Future playHistory( List items, { int initialIndex = 0, }) async { final media = items .where((i) => i.filePath.trim().isNotEmpty) .map(playableFromHistory) .toList(); if (media.isEmpty) return; await playAll(media, initialIndex: initialIndex.clamp(0, media.length - 1)); } Future playLocal( List items, { int initialIndex = 0, }) async { final media = items .where((i) => i.filePath.trim().isNotEmpty) .map(playableFromLocal) .toList(); if (media.isEmpty) return; await playAll(media, initialIndex: initialIndex.clamp(0, media.length - 1)); } Future play() async => _handler?.play(); Future pause() async => _handler?.pause(); Future stop() async => _handler?.stop(); Future seek(Duration position) async => _handler?.seek(position); Future next() async => _handler?.skipToNext(); Future previous() async => _handler?.skipToPrevious(); void setSleepTimer(Duration duration) => _handler?.setSleepTimer(duration); void cancelSleepTimer() => _handler?.cancelSleepTimer(); Future togglePlayPause(bool isPlaying) async { final handler = _handler; if (handler == null) return; final processingState = handler.playbackState.value.processingState; if (processingState == AudioProcessingState.loading || processingState == AudioProcessingState.buffering) { return; } if (isPlaying) { await handler.pause(); } else { await handler.play(); } } Future setShuffle(bool enabled) async { await _handler?.setShuffleMode( enabled ? AudioServiceShuffleMode.all : AudioServiceShuffleMode.none, ); } Future setRepeatMode(AudioServiceRepeatMode mode) async => _handler?.setRepeatMode(mode); Future playNext(PlayableMedia item) async => (await ensureInitialized())?.enqueue(item, playNext: true); Future addToQueue(PlayableMedia item) async => (await ensureInitialized())?.enqueue(item); Future playNextHistory(DownloadHistoryItem item) async => playNext(playableFromHistory(item)); Future addToQueueHistory(DownloadHistoryItem item) async => addToQueue(playableFromHistory(item)); Future playNextLocal(LocalLibraryItem item) async => playNext(playableFromLocal(item)); Future addToQueueLocal(LocalLibraryItem item) async => addToQueue(playableFromLocal(item)); Future jumpTo(int index) async => _handler?.skipToQueueItem(index); void moveQueueItem(int oldIndex, int newIndex) { _handler?.moveQueueItem(oldIndex, newIndex); } } final musicPlayerControllerProvider = Provider( (ref) => const MusicPlayerController(), ); PlayableMedia playableFromHistory(DownloadHistoryItem item) { return PlayableMedia( id: item.id, source: item.filePath, title: item.trackName, artist: item.artistName, album: item.albumName, artUri: (item.coverUrl != null && item.coverUrl!.trim().isNotEmpty) ? item.coverUrl : null, duration: (item.duration != null && item.duration! > 0) ? Duration(seconds: item.duration!) : null, bitDepth: item.bitDepth, sampleRate: item.sampleRate, bitrate: item.bitrate, format: item.format, explicit: item.explicit, ); } PlayableMedia playableFromLocal(LocalLibraryItem item) { String? art; final cover = item.coverPath; if (cover != null && cover.trim().isNotEmpty) { art = cover.startsWith('http') || cover.startsWith('content://') ? cover : Uri.file(cover).toString(); } return PlayableMedia( id: item.id, source: item.filePath, title: item.trackName, artist: item.artistName, album: item.albumName, artUri: art, duration: (item.duration != null && item.duration! > 0) ? Duration(seconds: item.duration!) : null, bitDepth: item.bitDepth, sampleRate: item.sampleRate, bitrate: item.bitrate, format: item.format, explicit: item.explicit, ); }