mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-14 00:15:17 +00:00

PlayerControls slider & duration are now vertical hotkey init moved to Home Player & YoutubeExplode are provided through riverpod Playback handles all things Player used to do GoRoutes are seperated from main to individual model file usePaletteColor bugfix occuring for before initilizing mount
246 lines
7.2 KiB
Dart
246 lines
7.2 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:just_audio/just_audio.dart';
|
|
import 'package:spotify/spotify.dart';
|
|
import 'package:spotube/helpers/search-youtube.dart';
|
|
import 'package:spotube/provider/AudioPlayer.dart';
|
|
import 'package:spotube/provider/YouTube.dart';
|
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
|
|
|
class CurrentPlaylist {
|
|
List<Track>? _tempTrack;
|
|
List<Track> tracks;
|
|
String id;
|
|
String name;
|
|
String thumbnail;
|
|
|
|
CurrentPlaylist({
|
|
required this.tracks,
|
|
required this.id,
|
|
required this.name,
|
|
required this.thumbnail,
|
|
});
|
|
|
|
List<String> get trackIds => tracks.map((e) => e.id!).toList();
|
|
|
|
void shuffle() {
|
|
// won't shuffle if already shuffled
|
|
if (_tempTrack == null) {
|
|
_tempTrack = [...tracks];
|
|
tracks.shuffle();
|
|
}
|
|
}
|
|
|
|
void unshuffle() {
|
|
// without _tempTracks unshuffling can't be done
|
|
if (_tempTrack != null) {
|
|
tracks = [..._tempTrack!];
|
|
_tempTrack = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
class Playback extends ChangeNotifier {
|
|
CurrentPlaylist? _currentPlaylist;
|
|
Track? _currentTrack;
|
|
|
|
// states
|
|
bool _isPlaying = false;
|
|
Duration? _duration;
|
|
|
|
// using custom listeners for duration as it changes super quickly
|
|
// which will cause re-renders in components that don't even need it
|
|
// thus only allowing to listen to change in duration through only
|
|
// a listener function
|
|
List<Function(Duration?)> _durationListeners = [];
|
|
|
|
// listeners
|
|
StreamSubscription<bool>? _playingStreamListener;
|
|
StreamSubscription<Duration?>? _durationStreamListener;
|
|
StreamSubscription<ProcessingState>? _processingStateStreamListener;
|
|
|
|
AudioPlayer player;
|
|
YoutubeExplode youtube;
|
|
Playback({
|
|
required this.player,
|
|
required this.youtube,
|
|
CurrentPlaylist? currentPlaylist,
|
|
Track? currentTrack,
|
|
}) : _currentPlaylist = currentPlaylist,
|
|
_currentTrack = currentTrack {
|
|
_playingStreamListener = player.playingStream.listen(
|
|
(playing) {
|
|
_isPlaying = playing;
|
|
notifyListeners();
|
|
},
|
|
);
|
|
|
|
_durationStreamListener = player.durationStream.listen((duration) async {
|
|
if (duration != null) {
|
|
// Actually things doesn't work all the time as they were
|
|
// described. So instead of listening to a `_ready`
|
|
// stream, it has to listen to duration stream since duration
|
|
// is always added to the Stream sink after all icyMetadata has
|
|
// been loaded thus indicating buffering started
|
|
if (duration != Duration.zero && duration != _duration) {
|
|
// this line is for prev/next or already playing playlist
|
|
if (player.playing) await player.pause();
|
|
await player.play();
|
|
}
|
|
_duration = duration;
|
|
_callAllDurationListeners(duration);
|
|
// for avoiding unnecessary re-renders in other components that
|
|
// doesn't need duration
|
|
}
|
|
});
|
|
|
|
_processingStateStreamListener =
|
|
player.processingStateStream.listen((event) async {
|
|
try {
|
|
if (event == ProcessingState.completed && _currentTrack?.id != null) {
|
|
movePlaylistPositionBy(1);
|
|
}
|
|
} catch (e, stack) {
|
|
print("[PrecessingStateStreamListener] $e");
|
|
print(stack);
|
|
}
|
|
});
|
|
}
|
|
|
|
CurrentPlaylist? get currentPlaylist => _currentPlaylist;
|
|
Track? get currentTrack => _currentTrack;
|
|
bool get isPlaying => _isPlaying;
|
|
|
|
/// this duration field is almost static & changes occasionally
|
|
///
|
|
/// If you want realtime duration with state-update/re-render
|
|
/// use custom state & the [addDurationChangeListener] function to do so
|
|
Duration? get duration => _duration;
|
|
|
|
_callAllDurationListeners(Duration? arg) {
|
|
for (var listener in _durationListeners) {
|
|
listener(arg);
|
|
}
|
|
}
|
|
|
|
void addDurationChangeListener(void Function(Duration? duration) listener) {
|
|
_durationListeners.add(listener);
|
|
}
|
|
|
|
void removeDurationChangeListener(
|
|
void Function(Duration? duration) listener) {
|
|
_durationListeners =
|
|
_durationListeners.where((p) => p != listener).toList();
|
|
}
|
|
|
|
set setCurrentTrack(Track track) {
|
|
_currentTrack = track;
|
|
notifyListeners();
|
|
}
|
|
|
|
set setCurrentPlaylist(CurrentPlaylist playlist) {
|
|
_currentPlaylist = playlist;
|
|
notifyListeners();
|
|
}
|
|
|
|
void reset() {
|
|
_isPlaying = false;
|
|
_duration = null;
|
|
_callAllDurationListeners(null);
|
|
_currentPlaylist = null;
|
|
_currentTrack = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// sets the provided id matched track's uri\
|
|
/// Doesn't notify listeners\
|
|
/// @returns `bool` - `true` if succeed & `false` when failed
|
|
bool setTrackUriById(String id, String uri) {
|
|
if (_currentPlaylist == null) return false;
|
|
try {
|
|
int index =
|
|
_currentPlaylist!.tracks.indexWhere((element) => element.id == id);
|
|
if (index == -1) return false;
|
|
_currentPlaylist!.tracks[index].uri = uri;
|
|
return _currentPlaylist!.tracks[index].uri == uri;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
dispose() {
|
|
_processingStateStreamListener?.cancel();
|
|
_durationStreamListener?.cancel();
|
|
_playingStreamListener?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
movePlaylistPositionBy(int pos) {
|
|
if (_currentTrack != null && _currentPlaylist != null) {
|
|
int index = _currentPlaylist!.trackIds.indexOf(_currentTrack!.id!) + pos;
|
|
|
|
var safeIndex = index > _currentPlaylist!.trackIds.length - 1
|
|
? 0
|
|
: index < 0
|
|
? _currentPlaylist!.trackIds.length
|
|
: index;
|
|
Track? track = _currentPlaylist!.tracks.asMap().containsKey(safeIndex)
|
|
? _currentPlaylist!.tracks.elementAt(safeIndex)
|
|
: null;
|
|
if (track != null) {
|
|
_duration = null;
|
|
_callAllDurationListeners(null);
|
|
_currentTrack = track;
|
|
notifyListeners();
|
|
// starts to play the newly entered next/prev track
|
|
startPlaying();
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> startPlaying([Track? track]) async {
|
|
try {
|
|
// the track is already playing so no need to change that
|
|
if (track != null && track.id == _currentTrack?.id) return;
|
|
track ??= _currentTrack;
|
|
if (track != null) {
|
|
Uri? parsedUri = Uri.tryParse(track.uri ?? "");
|
|
if (parsedUri != null && parsedUri.hasAbsolutePath) {
|
|
await player
|
|
.setAudioSource(
|
|
AudioSource.uri(parsedUri),
|
|
preload: true,
|
|
)
|
|
.then((value) async {
|
|
_currentTrack = track;
|
|
_duration = value;
|
|
_callAllDurationListeners(value);
|
|
notifyListeners();
|
|
});
|
|
}
|
|
final ytTrack = await toYoutubeTrack(youtube, track);
|
|
if (setTrackUriById(track.id!, ytTrack.uri!)) {
|
|
await player
|
|
.setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!)))
|
|
.then((value) {
|
|
_currentTrack = track;
|
|
notifyListeners();
|
|
});
|
|
}
|
|
}
|
|
} catch (e, stack) {
|
|
print("[Playback.startPlaying] $e");
|
|
print(stack);
|
|
}
|
|
}
|
|
}
|
|
|
|
final playbackProvider = ChangeNotifierProvider<Playback>((ref) {
|
|
final player = ref.watch(audioPlayerProvider);
|
|
final youtube = ref.watch(youtubeProvider);
|
|
return Playback(player: player, youtube: youtube);
|
|
});
|