diff --git a/lib/components/Album/AlbumCard.dart b/lib/components/Album/AlbumCard.dart index 0588fd49..8f723972 100644 --- a/lib/components/Album/AlbumCard.dart +++ b/lib/components/Album/AlbumCard.dart @@ -33,7 +33,10 @@ class AlbumCard extends HookConsumerWidget { "Album • ${artistsToString(album.artists ?? [])}", onTap: () { Navigator.of(context).push(SpotubePageRoute( - child: AlbumView(album), + child: AlbumView( + album, + key: Key("album-${album.id}"), + ), )); }, onPlaybuttonPressed: () async { diff --git a/lib/components/Artist/ArtistCard.dart b/lib/components/Artist/ArtistCard.dart index 6f92cb59..3d844c5c 100644 --- a/lib/components/Artist/ArtistCard.dart +++ b/lib/components/Artist/ArtistCard.dart @@ -18,7 +18,10 @@ class ArtistCard extends StatelessWidget { return InkWell( onTap: () { Navigator.of(context).push(SpotubePageRoute( - child: ArtistProfile(artist.id!), + child: ArtistProfile( + artist.id!, + key: Key("artist-${artist.id}"), + ), )); }, borderRadius: BorderRadius.circular(10), diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index 3b8c1f18..a8706689 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -247,6 +247,7 @@ class ArtistProfile extends HookConsumerWidget { child: ArtistAlbumView( artistId, snapshot.data?.name ?? "KRTX", + key: Key("artist-album-$artistId"), ), )); }, diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index 95d6e30b..cdbdfdf5 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -168,7 +168,7 @@ class Home extends HookConsumerWidget { Expanded(child: MoveWindow()), if (!Platform.isMacOS) const TitleBarActionButtons(), ], - )), + )) ], ), ), diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index 4cb651a4..8abb869d 100644 --- a/lib/components/Home/Sidebar.dart +++ b/lib/components/Home/Sidebar.dart @@ -31,7 +31,9 @@ class Sidebar extends HookConsumerWidget { static void goToSettings(BuildContext context) { Navigator.of(context).push(SpotubePageRoute( - child: const Settings(), + child: const Settings( + key: Key("settings"), + ), )); } diff --git a/lib/components/Lyrics.dart b/lib/components/Lyrics.dart index 71db1357..6cb5d83c 100644 --- a/lib/components/Lyrics.dart +++ b/lib/components/Lyrics.dart @@ -70,7 +70,9 @@ class Lyrics extends HookConsumerWidget { ElevatedButton( onPressed: () { Navigator.of(context).push(SpotubePageRoute( - child: const Settings(), + child: const Settings( + key: Key("settings"), + ), )); }, child: const Text("Add Access Token")) diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index 96c70f4f..1aec567c 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -4,13 +4,17 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:just_audio/just_audio.dart'; +import 'package:palette_generator/palette_generator.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Image; +import 'package:spotube/components/Player/PlayerOverlay.dart'; +import 'package:spotube/components/Player/PlayerTrackDetails.dart'; import 'package:spotube/components/Shared/DownloadTrackButton.dart'; import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/helpers/artists-to-clickable-artists.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/search-youtube.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; @@ -19,15 +23,17 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart'; class Player extends HookConsumerWidget { const Player({Key? key}) : super(key: key); - @override Widget build(BuildContext context, ref) { + Playback playback = ref.watch(playbackProvider); final _isPlaying = useState(false); final _shuffled = useState(false); final _volume = useState(0.0); final _duration = useState(null); final _currentTrackId = useState(null); + final breakpoint = useBreakpoints(); + final AudioPlayer player = useMemoized(() => AudioPlayer(), []); final YoutubeExplode youtube = useMemoized(() => YoutubeExplode(), []); final Future future = @@ -92,6 +98,7 @@ class Player extends HookConsumerWidget { print(stack); } }); + return () { playingStreamListener.cancel(); durationStreamListener.cancel(); @@ -106,9 +113,11 @@ class Player extends HookConsumerWidget { _volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ?? player.volume; } + return null; }, [localStorage.data]); - var _playTrack = useCallback((Track currentTrack, Playback playback) async { + final _playTrack = + useCallback((Track currentTrack, Playback playback) async { try { if (currentTrack.id != _currentTrackId.value) { Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? ""); @@ -140,6 +149,13 @@ class Player extends HookConsumerWidget { } }, [player, _currentTrackId, _duration]); + useEffect(() { + if (playback.currentPlaylist != null && playback.currentTrack != null) { + _playTrack(playback.currentTrack!, playback); + } + return null; + }, [playback.currentPlaylist, playback.currentTrack]); + var _onNext = useCallback(() async { try { await player.pause(); @@ -162,193 +178,202 @@ class Player extends HookConsumerWidget { } }, [player]); + String albumArt = useMemoized( + () => imageToUrlString( + playback.currentTrack?.album?.images, + index: (playback.currentTrack?.album?.images?.length ?? 1) - 1, + ), + [playback.currentTrack?.album?.images], + ); + + final entryRef = useRef(null); + + disposeOverlay() { + try { + entryRef.value?.remove(); + entryRef.value = null; + } catch (e, stack) { + if (e is! AssertionError) { + print("[Player.useEffect.cleanup] $e"); + print(stack); + } + } + } + + final controls = PlayerControls( + positionStream: player.positionStream, + isPlaying: _isPlaying.value, + duration: _duration.value ?? Duration.zero, + shuffled: _shuffled.value, + onNext: _onNext, + onPrevious: _onPrevious, + onPause: () async { + try { + await player.pause(); + } catch (e, stack) { + print("[PlayerControls.onPause()] $e"); + print(stack); + } + }, + onPlay: () async { + try { + await player.play(); + } catch (e, stack) { + print("[PlayerControls.onPlay()] $e"); + print(stack); + } + }, + onSeek: (value) async { + try { + await player.seek(Duration(seconds: value.toInt())); + } catch (e, stack) { + print("[PlayerControls.onSeek()] $e"); + print(stack); + } + }, + onShuffle: () async { + if (playback.currentTrack == null || playback.currentPlaylist == null) { + return; + } + try { + if (!_shuffled.value) { + playback.currentPlaylist!.shuffle(); + _shuffled.value = true; + } else { + playback.currentPlaylist!.unshuffle(); + _shuffled.value = false; + } + } catch (e, stack) { + print("[PlayerControls.onShuffle()] $e"); + print(stack); + } + }, + onStop: () async { + try { + await player.pause(); + await player.seek(Duration.zero); + _isPlaying.value = false; + _currentTrackId.value = null; + _duration.value = null; + _shuffled.value = false; + playback.reset(); + } catch (e, stack) { + print("[PlayerControls.onStop()] $e"); + print(stack); + } + }, + ); + + useEffect(() { + // clearing the overlay-entry as passing the already available + // entry will result in splashing while resizing the window + if (entryRef.value != null) disposeOverlay(); + if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) { + entryRef.value = OverlayEntry( + opaque: false, + builder: (context) => PlayerOverlay( + controls: controls, + albumArt: albumArt, + ), + ); + // I can't believe useEffect doesn't run Post Frame aka + // after rendering/painting the UI + // `My disappointment is immeasurable and my day is ruined` XD + WidgetsBinding.instance?.addPostFrameCallback((time) { + Overlay.of(context)?.insert(entryRef.value!); + }); + } + return () { + disposeOverlay(); + }; + }, [breakpoint]); + + // returning an empty non spacious Container as the overlay will take + // place in the global overlay stack aka [_entries] + if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) { + return Container(); + } + return Container( color: Theme.of(context).backgroundColor, - child: HookConsumer( - builder: (context, ref, widget) { - Playback playback = ref.watch(playbackProvider); - if (playback.currentPlaylist != null && - playback.currentTrack != null) { - _playTrack(playback.currentTrack!, playback); - } - - String? albumArt = useMemoized( - () => imageToUrlString( - playback.currentTrack?.album?.images, - index: (playback.currentTrack?.album?.images?.length ?? 1) - 1, + child: Material( + type: MaterialType.transparency, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded(child: PlayerTrackDetails(albumArt: albumArt)), + // controls + Flexible( + flex: 3, + child: controls, ), - [playback.currentTrack?.album?.images], - ); - - return Material( - type: MaterialType.transparency, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (albumArt != null) - CachedNetworkImage( - imageUrl: albumArt, - maxHeightDiskCache: 50, - maxWidthDiskCache: 50, - placeholder: (context, url) { - return Container( - height: 50, - width: 50, - color: Colors.green[400], - ); - }, - ), - // title of the currently playing track - Flexible( - flex: 1, - child: Column( - children: [ - Text( - playback.currentTrack?.name ?? "Not playing", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - artistsToClickableArtists( - playback.currentTrack?.artists ?? [], - mainAxisAlignment: MainAxisAlignment.center, - ) - ], - ), - ), - // controls - Flexible( - flex: 3, - child: PlayerControls( - positionStream: player.positionStream, - isPlaying: _isPlaying.value, - duration: _duration.value ?? Duration.zero, - shuffled: _shuffled.value, - onNext: _onNext, - onPrevious: _onPrevious, - onPause: () async { - try { - await player.pause(); - } catch (e, stack) { - print("[PlayerControls.onPause()] $e"); - print(stack); - } - }, - onPlay: () async { - try { - await player.play(); - } catch (e, stack) { - print("[PlayerControls.onPlay()] $e"); - print(stack); - } - }, - onSeek: (value) async { - try { - await player.seek(Duration(seconds: value.toInt())); - } catch (e, stack) { - print("[PlayerControls.onSeek()] $e"); - print(stack); - } - }, - onShuffle: () async { - if (playback.currentTrack == null || - playback.currentPlaylist == null) return; - try { - if (!_shuffled.value) { - playback.currentPlaylist!.shuffle(); - _shuffled.value = true; - } else { - playback.currentPlaylist!.unshuffle(); - _shuffled.value = false; + // add to saved tracks + Expanded( + flex: 1, + child: Wrap( + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + Container( + height: 20, + constraints: const BoxConstraints(maxWidth: 200), + child: Slider.adaptive( + value: _volume.value, + onChanged: (value) async { + try { + await player.setVolume(value).then((_) { + _volume.value = value; + localStorage.data?.setDouble( + LocalStorageKeys.volume, + value, + ); + }); + } catch (e, stack) { + print("[VolumeSlider.onChange()] $e"); + print(stack); } - } catch (e, stack) { - print("[PlayerControls.onShuffle()] $e"); - print(stack); - } - }, - onStop: () async { - try { - await player.pause(); - await player.seek(Duration.zero); - _isPlaying.value = false; - _currentTrackId.value = null; - _duration.value = null; - _shuffled.value = false; - playback.reset(); - } catch (e, stack) { - print("[PlayerControls.onStop()] $e"); - print(stack); - } - }, + }, + ), ), - ), - // add to saved tracks - Expanded( - flex: 1, - child: Wrap( - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - height: 20, - constraints: const BoxConstraints(maxWidth: 200), - child: Slider.adaptive( - value: _volume.value, - onChanged: (value) async { - try { - await player.setVolume(value).then((_) { - _volume.value = value; - localStorage.data?.setDouble( - LocalStorageKeys.volume, - value, - ); - }); - } catch (e, stack) { - print("[VolumeSlider.onChange()] $e"); - print(stack); - } - }, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - DownloadTrackButton( - track: playback.currentTrack, - ), - Consumer(builder: (context, ref, widget) { - SpotifyApi spotifyApi = ref.watch(spotifyProvider); - return FutureBuilder( - future: playback.currentTrack?.id != null - ? spotifyApi.tracks.me - .containsOne(playback.currentTrack!.id!) - : Future.value(false), - initialData: false, - builder: (context, snapshot) { - bool isLiked = snapshot.data ?? false; - return IconButton( - icon: Icon( - !isLiked - ? Icons.favorite_outline_rounded - : Icons.favorite_rounded, - color: isLiked ? Colors.green : null, - ), - onPressed: () { - if (!isLiked && - playback.currentTrack?.id != null) { - spotifyApi.tracks.me.saveOne( - playback.currentTrack!.id!); - } - }); - }); - }), - ], + DownloadTrackButton( + track: playback.currentTrack, ), + Consumer(builder: (context, ref, widget) { + SpotifyApi spotifyApi = ref.watch(spotifyProvider); + return FutureBuilder( + future: playback.currentTrack?.id != null + ? spotifyApi.tracks.me + .containsOne(playback.currentTrack!.id!) + : Future.value(false), + initialData: false, + builder: (context, snapshot) { + bool isLiked = snapshot.data ?? false; + return IconButton( + icon: Icon( + !isLiked + ? Icons.favorite_outline_rounded + : Icons.favorite_rounded, + color: isLiked ? Colors.green : null, + ), + onPressed: () { + if (!isLiked && + playback.currentTrack?.id != null) { + spotifyApi.tracks.me + .saveOne(playback.currentTrack!.id!); + } + }); + }); + }), ], ), - ) - ], - ), - ); - }, + ], + ), + ) + ], + ), ), ); } diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart index 4583a98f..0aac81dd 100644 --- a/lib/components/Player/PlayerControls.dart +++ b/lib/components/Player/PlayerControls.dart @@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/GlobalKeyActions.dart'; import 'package:spotube/provider/UserPreferences.dart'; @@ -77,79 +78,91 @@ class PlayerControls extends HookConsumerWidget { }; }); - return Container( - constraints: const BoxConstraints(maxWidth: 700), - child: Column( - children: [ - StreamBuilder( - stream: positionStream, - builder: (context, snapshot) { - var totalMinutes = - zeroPadNumStr(duration.inMinutes.remainder(60)); - var totalSeconds = - zeroPadNumStr(duration.inSeconds.remainder(60)); - var currentMinutes = snapshot.hasData - ? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60)) - : "00"; - var currentSeconds = snapshot.hasData - ? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60)) - : "00"; + final breakpoint = useBreakpoints(); - var sliderMax = duration.inSeconds; - var sliderValue = snapshot.data?.inSeconds ?? 0; - return Row( - children: [ - Expanded( - child: Slider.adaptive( - // cannot divide by zero - // there's an edge case for value being bigger - // than total duration. Keeping it resolved - value: (sliderMax == 0 || sliderValue > sliderMax) - ? 0 - : sliderValue / sliderMax, - onChanged: (value) {}, - onChangeEnd: (value) { - onSeek?.call(value * sliderMax); - }, - ), - ), - Text( - "$currentMinutes:$currentSeconds/$totalMinutes:$totalSeconds", - ) - ], - ); + Widget controlButtons = Material( + type: MaterialType.transparency, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (breakpoint.isMoreThan(Breakpoints.md)) + IconButton( + icon: const Icon(Icons.shuffle_rounded), + color: shuffled ? Theme.of(context).primaryColor : null, + onPressed: () { + onShuffle?.call(); + }), + IconButton( + icon: const Icon(Icons.skip_previous_rounded), + onPressed: () { + onPrevious?.call(); }), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - icon: const Icon(Icons.shuffle_rounded), - color: shuffled ? Theme.of(context).primaryColor : null, - onPressed: () { - onShuffle?.call(); - }), - IconButton( - icon: const Icon(Icons.skip_previous_rounded), - onPressed: () { - onPrevious?.call(); - }), - IconButton( - icon: Icon( - isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, - ), - onPressed: () => _playOrPause(null), - ), - IconButton( - icon: const Icon(Icons.skip_next_rounded), - onPressed: () => onNext?.call()), - IconButton( - icon: const Icon(Icons.stop_rounded), - onPressed: () => onStop?.call(), - ) - ], - ) + IconButton( + icon: Icon( + isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, + ), + onPressed: () => _playOrPause(null), + ), + IconButton( + icon: const Icon(Icons.skip_next_rounded), + onPressed: () => onNext?.call()), + if (breakpoint.isMoreThan(Breakpoints.md)) + IconButton( + icon: const Icon(Icons.stop_rounded), + onPressed: () => onStop?.call(), + ) ], ), ); + + if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) { + return controlButtons; + } + + return Container( + constraints: const BoxConstraints(maxWidth: 700), + child: Column( + children: [ + StreamBuilder( + stream: positionStream, + builder: (context, snapshot) { + var totalMinutes = + zeroPadNumStr(duration.inMinutes.remainder(60)); + var totalSeconds = + zeroPadNumStr(duration.inSeconds.remainder(60)); + var currentMinutes = snapshot.hasData + ? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60)) + : "00"; + var currentSeconds = snapshot.hasData + ? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60)) + : "00"; + + var sliderMax = duration.inSeconds; + var sliderValue = snapshot.data?.inSeconds ?? 0; + return Row( + children: [ + Expanded( + child: Slider.adaptive( + // cannot divide by zero + // there's an edge case for value being bigger + // than total duration. Keeping it resolved + value: (sliderMax == 0 || sliderValue > sliderMax) + ? 0 + : sliderValue / sliderMax, + onChanged: (value) {}, + onChangeEnd: (value) { + onSeek?.call(value * sliderMax); + }, + ), + ), + Text( + "$currentMinutes:$currentSeconds/$totalMinutes:$totalSeconds", + ) + ], + ); + }), + controlButtons, + ], + )); } } diff --git a/lib/components/Player/PlayerOverlay.dart b/lib/components/Player/PlayerOverlay.dart new file mode 100644 index 00000000..1521fac8 --- /dev/null +++ b/lib/components/Player/PlayerOverlay.dart @@ -0,0 +1,55 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:palette_generator/palette_generator.dart'; +import 'package:spotube/components/Player/PlayerTrackDetails.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; + +class PlayerOverlay extends HookWidget { + final Widget controls; + final String albumArt; + const PlayerOverlay({ + required this.controls, + required this.albumArt, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final breakpoint = useBreakpoints(); + + return Positioned( + right: (breakpoint.isMd ? 10 : 5), + left: (breakpoint.isSm ? 5 : 80), + bottom: (breakpoint.isSm ? 63 : 10), + child: FutureBuilder( + future: PaletteGenerator.fromImageProvider( + CachedNetworkImageProvider( + albumArt, + cacheKey: albumArt, + maxHeight: 50, + maxWidth: 50, + ), + ), + builder: (context, snapshot) { + return Container( + width: MediaQuery.of(context).size.width, + height: 50, + decoration: BoxDecoration( + color: snapshot.hasData + ? snapshot.data!.colors.first + : Colors.blueGrey[200], + borderRadius: BorderRadius.circular(5), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PlayerTrackDetails(albumArt: albumArt), + controls, + ], + ), + ); + }), + ); + } +} diff --git a/lib/components/Player/PlayerTrackDetails.dart b/lib/components/Player/PlayerTrackDetails.dart new file mode 100644 index 00000000..9d4962a1 --- /dev/null +++ b/lib/components/Player/PlayerTrackDetails.dart @@ -0,0 +1,71 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/helpers/artists-to-clickable-artists.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/provider/Playback.dart'; + +class PlayerTrackDetails extends HookConsumerWidget { + final String? albumArt; + const PlayerTrackDetails({Key? key, this.albumArt}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final breakpoint = useBreakpoints(); + final playback = ref.watch(playbackProvider); + + return Row( + children: [ + if (albumArt != null) + Padding( + padding: EdgeInsets.all( + breakpoint.isLessThanOrEqualTo(Breakpoints.md) ? 5.0 : 0), + child: CachedNetworkImage( + imageUrl: albumArt!, + maxHeightDiskCache: 50, + maxWidthDiskCache: 50, + cacheKey: albumArt, + placeholder: (context, url) { + return Container( + height: 50, + width: 50, + color: Colors.green[400], + ); + }, + ), + ), + if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) ...[ + const SizedBox(width: 10), + Text( + playback.currentTrack?.name ?? "Not playing", + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodyText1 + ?.copyWith(fontWeight: FontWeight.bold), + ), + ], + // title of the currently playing track + if (breakpoint.isMoreThan(Breakpoints.md)) + Flexible( + flex: 1, + child: Column( + children: [ + Text( + playback.currentTrack?.name ?? "Not playing", + style: Theme.of(context) + .textTheme + .bodyText1 + ?.copyWith(fontWeight: FontWeight.bold), + ), + artistsToClickableArtists( + playback.currentTrack?.artists ?? [], + mainAxisAlignment: MainAxisAlignment.center, + ) + ], + ), + ), + ], + ); + } +} diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index 82c52cc2..5ba07808 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -26,9 +26,14 @@ class PlaylistCard extends HookConsumerWidget { imageUrl: playlist.images![0].url!, isPlaying: isPlaylistPlaying, onTap: () { - Navigator.of(context).push(SpotubePageRoute( - child: PlaylistView(playlist), - )); + Navigator.of(context).push( + SpotubePageRoute( + child: PlaylistView( + playlist, + key: Key("playlist-${playlist.id}"), + ), + ), + ); }, onPlaybuttonPressed: () async { if (isPlaylistPlaying) return; diff --git a/lib/components/Shared/SpotubePageRoute.dart b/lib/components/Shared/SpotubePageRoute.dart index 0cc47129..69a37da5 100644 --- a/lib/components/Shared/SpotubePageRoute.dart +++ b/lib/components/Shared/SpotubePageRoute.dart @@ -4,8 +4,8 @@ class SpotubePageRoute extends PageRouteBuilder { final Widget child; SpotubePageRoute({required this.child}) : super( - pageBuilder: (context, animation, secondaryAnimation) => child, - ); + pageBuilder: (context, animation, secondaryAnimation) => child, + settings: RouteSettings(name: child.key.toString())); @override Widget buildTransitions(BuildContext context, Animation animation, diff --git a/lib/helpers/image-to-url-string.dart b/lib/helpers/image-to-url-string.dart index f9cf0938..0d4cc397 100644 --- a/lib/helpers/image-to-url-string.dart +++ b/lib/helpers/image-to-url-string.dart @@ -1,7 +1,9 @@ import 'package:spotify/spotify.dart'; +import 'package:uuid/uuid.dart' show Uuid; +const uuid = Uuid(); String imageToUrlString(List? images, {int index = 0}) { return images != null && images.isNotEmpty ? images[0].url! - : "https://avatars.dicebear.com/api/croodles-neutral/${DateTime.now().toString()}.png"; + : "https://avatars.dicebear.com/api/croodles-neutral/${uuid.v4()}.png"; } diff --git a/pubspec.lock b/pubspec.lock index 0ad07331..0d670b24 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -226,6 +226,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + go_router: + dependency: "direct main" + description: + name: go_router + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4" hooks_riverpod: dependency: "direct main" description: @@ -331,6 +338,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" matcher: dependency: transitive description: @@ -380,6 +394,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" + palette_generator: + dependency: "direct main" + description: + name: palette_generator + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" path: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7ff5c832..cfcedea6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,8 @@ dependencies: flutter_riverpod: ^1.0.3 flutter_hooks: ^0.18.2+1 hooks_riverpod: ^1.0.3 + go_router: ^3.0.4 + palette_generator: ^0.3.3 dev_dependencies: flutter_test: