hooks logic implmented in Player component

This commit is contained in:
Kingkor Roy Tirtho 2022-02-11 20:07:49 +06:00
parent 9fc155c000
commit 7d280f92ce

View File

@ -1,7 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/DownloadTrackButton.dart'; import 'package:spotube/components/Shared/DownloadTrackButton.dart';
@ -14,103 +15,21 @@ import 'package:flutter/material.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
class Player extends ConsumerStatefulWidget { class Player extends HookConsumerWidget {
const Player({Key? key}) : super(key: key); const Player({Key? key}) : super(key: key);
@override @override
_PlayerState createState() => _PlayerState(); Widget build(BuildContext context, ref) {
} var _isPlaying = useState(false);
var _shuffled = useState(false);
var _volume = useState(0.0);
var _duration = useState<Duration?>(null);
var _currentTrackId = useState<String?>(null);
class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver { AudioPlayer player = useMemoized(() => AudioPlayer(), []);
late AudioPlayer player; YoutubeExplode youtube = useMemoized(() => YoutubeExplode(), []);
bool _isPlaying = false;
bool _shuffled = false;
Duration? _duration;
String? _currentTrackId; var _movePlaylistPositionBy = useCallback((int pos) {
double _volume = 0;
late YoutubeExplode youtube;
@override
void initState() {
try {
super.initState();
player = AudioPlayer();
youtube = YoutubeExplode();
WidgetsBinding.instance?.addObserver(this);
WidgetsBinding.instance?.addPostFrameCallback(_init);
} catch (e, stack) {
print("[Player.initState()] $e");
print(stack);
}
}
_init(Duration timeStamp) async {
try {
setState(() {
_volume = player.volume;
});
player.playingStream.listen((playing) async {
setState(() {
_isPlaying = playing;
});
});
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 `playback.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();
}
setState(() {
_duration = duration;
});
}
});
player.processingStateStream.listen((event) async {
try {
if (event == ProcessingState.completed && _currentTrackId != null) {
_movePlaylistPositionBy(1);
}
} catch (e, stack) {
print("[PrecessingStateStreamListener] $e");
print(stack);
}
});
} catch (e) {
print("[Player._init()]: $e");
}
}
@override
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
player.dispose();
youtube.close();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
// Release the player's resources when not in use. We use "stop" so that
// if the app resumes later, it will still remember what position to
// resume from.
player.stop();
}
}
void _movePlaylistPositionBy(int pos) {
Playback playback = ref.read(playbackProvider); Playback playback = ref.read(playbackProvider);
if (playback.currentTrack != null && playback.currentPlaylist != null) { if (playback.currentTrack != null && playback.currentPlaylist != null) {
int index = playback.currentPlaylist!.trackIds int index = playback.currentPlaylist!.trackIds
@ -128,16 +47,59 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
: null; : null;
if (track != null) { if (track != null) {
playback.setCurrentTrack = track; playback.setCurrentTrack = track;
setState(() { _duration.value = null;
_duration = null;
});
}
} }
} }
}, [_duration]);
Future _playTrack(Track currentTrack, Playback playback) async { useEffect(() {
_volume.value = player.volume;
var playingStreamListener = player.playingStream.listen((playing) async {
_isPlaying.value = playing;
});
var 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 `playback.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.value) {
// this line is for prev/next or already playing playlist
if (player.playing) await player.pause();
await player.play();
}
_duration.value = duration;
}
});
var processingStateStreamListener =
player.processingStateStream.listen((event) async {
try { try {
if (currentTrack.id != _currentTrackId) { if (event == ProcessingState.completed &&
_currentTrackId.value != null) {
_movePlaylistPositionBy(1);
}
} catch (e, stack) {
print("[PrecessingStateStreamListener] $e");
print(stack);
}
});
return () {
playingStreamListener.cancel();
durationStreamListener.cancel();
processingStateStreamListener.cancel();
player.dispose();
youtube.close();
};
}, []);
var _playTrack = useCallback((Track currentTrack, Playback playback) async {
try {
if (currentTrack.id != _currentTrackId.value) {
Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? ""); Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? "");
if (parsedUri != null && parsedUri.hasAbsolutePath) { if (parsedUri != null && parsedUri.hasAbsolutePath) {
await player await player
@ -146,22 +108,18 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
preload: true, preload: true,
) )
.then((value) async { .then((value) async {
setState(() { _currentTrackId.value = currentTrack.id;
_currentTrackId = currentTrack.id; if (_duration.value != null) {
if (_duration != null) { _duration.value = value;
_duration = value;
} }
}); });
});
} }
var ytTrack = await toYoutubeTrack(youtube, currentTrack); var ytTrack = await toYoutubeTrack(youtube, currentTrack);
if (playback.setTrackUriById(currentTrack.id!, ytTrack.uri!)) { if (playback.setTrackUriById(currentTrack.id!, ytTrack.uri!)) {
await player await player
.setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!))) .setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!)))
.then((value) { .then((value) {
setState(() { _currentTrackId.value = currentTrack.id;
_currentTrackId = currentTrack.id;
});
}); });
} }
} }
@ -169,9 +127,9 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
print("[Player._playTrack()] $e"); print("[Player._playTrack()] $e");
print(stack); print(stack);
} }
} }, [player, _currentTrackId, _duration]);
_onNext() async { var _onNext = useCallback(() async {
try { try {
await player.pause(); await player.pause();
await player.seek(Duration.zero); await player.seek(Duration.zero);
@ -180,9 +138,9 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
print("[PlayerControls.onNext()] $e"); print("[PlayerControls.onNext()] $e");
print(stack); print(stack);
} }
} }, [player]);
_onPrevious() async { var _onPrevious = useCallback(() async {
try { try {
await player.pause(); await player.pause();
await player.seek(Duration.zero); await player.seek(Duration.zero);
@ -191,13 +149,11 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
print("[PlayerControls.onPrevious()] $e"); print("[PlayerControls.onPrevious()] $e");
print(stack); print(stack);
} }
} }, [player]);
@override
Widget build(BuildContext context) {
return Container( return Container(
color: Theme.of(context).backgroundColor, color: Theme.of(context).backgroundColor,
child: Consumer( child: HookConsumer(
builder: (context, ref, widget) { builder: (context, ref, widget) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
if (playback.currentPlaylist != null && if (playback.currentPlaylist != null &&
@ -205,9 +161,12 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
_playTrack(playback.currentTrack!, playback); _playTrack(playback.currentTrack!, playback);
} }
String? albumArt = imageToUrlString( String? albumArt = useMemoized(
() => imageToUrlString(
playback.currentTrack?.album?.images, playback.currentTrack?.album?.images,
index: (playback.currentTrack?.album?.images?.length ?? 1) - 1, index: (playback.currentTrack?.album?.images?.length ?? 1) - 1,
),
[playback.currentTrack?.album?.images],
); );
return Material( return Material(
@ -249,9 +208,9 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
flex: 3, flex: 3,
child: PlayerControls( child: PlayerControls(
positionStream: player.positionStream, positionStream: player.positionStream,
isPlaying: _isPlaying, isPlaying: _isPlaying.value,
duration: _duration ?? Duration.zero, duration: _duration.value ?? Duration.zero,
shuffled: _shuffled, shuffled: _shuffled.value,
onNext: _onNext, onNext: _onNext,
onPrevious: _onPrevious, onPrevious: _onPrevious,
onPause: () async { onPause: () async {
@ -282,16 +241,12 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
if (playback.currentTrack == null || if (playback.currentTrack == null ||
playback.currentPlaylist == null) return; playback.currentPlaylist == null) return;
try { try {
if (!_shuffled) { if (!_shuffled.value) {
playback.currentPlaylist!.shuffle(); playback.currentPlaylist!.shuffle();
setState(() { _shuffled.value = true;
_shuffled = true;
});
} else { } else {
playback.currentPlaylist!.unshuffle(); playback.currentPlaylist!.unshuffle();
setState(() { _shuffled.value = false;
_shuffled = false;
});
} }
} catch (e, stack) { } catch (e, stack) {
print("[PlayerControls.onShuffle()] $e"); print("[PlayerControls.onShuffle()] $e");
@ -302,12 +257,10 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
try { try {
await player.pause(); await player.pause();
await player.seek(Duration.zero); await player.seek(Duration.zero);
setState(() { _isPlaying.value = false;
_isPlaying = false; _currentTrackId.value = null;
_currentTrackId = null; _duration.value = null;
_duration = null; _shuffled.value = false;
_shuffled = false;
});
playback.reset(); playback.reset();
} catch (e, stack) { } catch (e, stack) {
print("[PlayerControls.onStop()] $e"); print("[PlayerControls.onStop()] $e");
@ -327,13 +280,11 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
height: 20, height: 20,
constraints: const BoxConstraints(maxWidth: 200), constraints: const BoxConstraints(maxWidth: 200),
child: Slider.adaptive( child: Slider.adaptive(
value: _volume, value: _volume.value,
onChanged: (value) async { onChanged: (value) async {
try { try {
await player.setVolume(value).then((_) { await player.setVolume(value).then((_) {
setState(() { _volume.value = value;
_volume = value;
});
}); });
} catch (e, stack) { } catch (e, stack) {
print("[VolumeSlider.onChange()] $e"); print("[VolumeSlider.onChange()] $e");
@ -368,10 +319,8 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
onPressed: () { onPressed: () {
if (!isLiked && if (!isLiked &&
playback.currentTrack?.id != null) { playback.currentTrack?.id != null) {
spotifyApi.tracks.me spotifyApi.tracks.me.saveOne(
.saveOne( playback.currentTrack!.id!);
playback.currentTrack!.id!)
.then((value) => setState(() {}));
} }
}); });
}); });