/// Music Provider - Player state management library; import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:just_audio/just_audio.dart'; import '../../../domain/entities/track.dart'; /// Player state class PlayerState { final List queue; final int currentIndex; final bool isPlaying; final Duration position; final Duration duration; final bool isLoading; final String? errorMessage; const PlayerState({ this.queue = const [], this.currentIndex = -1, this.isPlaying = false, this.position = Duration.zero, this.duration = Duration.zero, this.isLoading = false, this.errorMessage, }); Track? get currentTrack => currentIndex >= 0 && currentIndex < queue.length ? queue[currentIndex] : null; PlayerState copyWith({ List? queue, int? currentIndex, bool? isPlaying, Duration? position, Duration? duration, bool? isLoading, String? errorMessage, }) { return PlayerState( queue: queue ?? this.queue, currentIndex: currentIndex ?? this.currentIndex, isPlaying: isPlaying ?? this.isPlaying, position: position ?? this.position, duration: duration ?? this.duration, isLoading: isLoading ?? this.isLoading, errorMessage: errorMessage, ); } } /// Player notifier class PlayerNotifier extends StateNotifier { PlayerNotifier() : super(const PlayerState()) { _player = AudioPlayer(); _subscriptions = []; _init(); } late final AudioPlayer _player; final List _subscriptions = []; void _init() { // Subscribe to position stream and store subscription _subscriptions.add(_player.positionStream.listen((position) { state = state.copyWith(position: position); })); // Subscribe to duration stream and store subscription _subscriptions.add(_player.durationStream.listen((duration) { state = state.copyWith(duration: duration ?? Duration.zero); })); // Subscribe to player state stream and store subscription _subscriptions.add(_player.playerStateStream.listen((playerState) { state = state.copyWith( isPlaying: playerState.playing, isLoading: playerState.processingState == ProcessingState.loading, ); })); } Future loadTrack(Track track) async { state = state.copyWith(isLoading: true, errorMessage: null); try { // Validate audio URL exists final streamUrl = track.audioUrl; if (streamUrl == null || streamUrl.isEmpty) { throw Exception('No audio URL available for track: ${track.title}'); } await _player.setUrl(streamUrl); if (state.queue.isEmpty) { state = state.copyWith(queue: [track], currentIndex: 0); } // Clear error and loading state on success state = state.copyWith(isLoading: false, errorMessage: null); } on PlayerException catch (e) { // Specific audio player errors debugPrint('Player error loading track: ${e.message}'); state = state.copyWith( isLoading: false, errorMessage: 'Unable to play this track. Please try another.', ); } catch (e) { // Network or other errors debugPrint('Error loading track: $e'); state = state.copyWith( isLoading: false, errorMessage: 'An error occurred while loading the track.', ); } } Future play() async { if (state.currentTrack != null) { await _player.play(); state = state.copyWith(isPlaying: true); } } Future pause() async { await _player.pause(); state = state.copyWith(isPlaying: false); } /// Convenience method to toggle play/pause Future togglePlay() async { if (state.isPlaying) { await pause(); } else { await play(); } } Future seek(Duration position) async { await _player.seek(position); } Future next() async { if (state.currentIndex < state.queue.length - 1) { final nextTrack = state.queue[state.currentIndex + 1]; await loadTrack(nextTrack); state = state.copyWith(currentIndex: state.currentIndex + 1); await play(); } } Future previous() async { if (state.currentIndex > 0) { final previousTrack = state.queue[state.currentIndex - 1]; await loadTrack(previousTrack); state = state.copyWith(currentIndex: state.currentIndex - 1); await play(); } } void setQueue(List tracks, {int startIndex = 0}) { state = state.copyWith( queue: tracks, currentIndex: startIndex, ); } void addToQueue(Track track) { final newQueue = [...state.queue, track]; state = state.copyWith(queue: newQueue); } void removeFromQueue(int index) { if (index >= 0 && index < state.queue.length) { final newQueue = [...state.queue]..removeAt(index); state = state.copyWith(queue: newQueue); } } @override void dispose() { // Cancel all stream subscriptions to prevent memory leaks for (final subscription in _subscriptions) { subscription.cancel(); } _player.dispose(); super.dispose(); } } /// Player provider final playerProvider = StateNotifierProvider((ref) { return PlayerNotifier(); }); /// Current track provider final currentTrackProvider = Provider((ref) { return ref.watch(playerProvider).currentTrack; }); /// Queue view data class class QueueViewData { final Track? currentTrack; final List queue; final int currentIndex; final bool isPlaying; const QueueViewData({ required this.currentTrack, required this.queue, required this.currentIndex, required this.isPlaying, }); /// Get upcoming tracks (after current) List get nextTracks { if (currentIndex < 0 || currentIndex >= queue.length - 1) { return []; } return queue.sublist(currentIndex + 1); } /// Get previously played tracks (before current) List get previousTracks { if (currentIndex <= 0) { return []; } return queue.sublist(0, currentIndex); } /// Check if queue has tracks bool get hasQueue => queue.isNotEmpty; /// Check if there are upcoming tracks bool get hasNextTracks => nextTracks.isNotEmpty; /// Check if there are previous tracks bool get hasPreviousTracks => previousTracks.isNotEmpty; /// Get total queue count excluding current int get queueCount => queue.length - 1; } /// Queue view provider final queueProvider = Provider((ref) { final playerState = ref.watch(playerProvider); return QueueViewData( currentTrack: playerState.currentTrack, queue: playerState.queue, currentIndex: playerState.currentIndex, isPlaying: playerState.isPlaying, ); });