PlayerOverlay works as expected

imageToUrlString uses uuid instead of DateTime.now()
seperated parts of Player for reuse accross different sizes of screen's specific widgets
integrating go_router to follow declarative route approach
This commit is contained in:
Kingkor Roy Tirtho 2022-03-10 12:29:29 +06:00
parent b585bf2df2
commit d608fa7d02
15 changed files with 466 additions and 261 deletions

View File

@ -33,7 +33,10 @@ class AlbumCard extends HookConsumerWidget {
"Album • ${artistsToString<ArtistSimple>(album.artists ?? [])}", "Album • ${artistsToString<ArtistSimple>(album.artists ?? [])}",
onTap: () { onTap: () {
Navigator.of(context).push(SpotubePageRoute( Navigator.of(context).push(SpotubePageRoute(
child: AlbumView(album), child: AlbumView(
album,
key: Key("album-${album.id}"),
),
)); ));
}, },
onPlaybuttonPressed: () async { onPlaybuttonPressed: () async {

View File

@ -18,7 +18,10 @@ class ArtistCard extends StatelessWidget {
return InkWell( return InkWell(
onTap: () { onTap: () {
Navigator.of(context).push(SpotubePageRoute( Navigator.of(context).push(SpotubePageRoute(
child: ArtistProfile(artist.id!), child: ArtistProfile(
artist.id!,
key: Key("artist-${artist.id}"),
),
)); ));
}, },
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),

View File

@ -247,6 +247,7 @@ class ArtistProfile extends HookConsumerWidget {
child: ArtistAlbumView( child: ArtistAlbumView(
artistId, artistId,
snapshot.data?.name ?? "KRTX", snapshot.data?.name ?? "KRTX",
key: Key("artist-album-$artistId"),
), ),
)); ));
}, },

View File

@ -168,7 +168,7 @@ class Home extends HookConsumerWidget {
Expanded(child: MoveWindow()), Expanded(child: MoveWindow()),
if (!Platform.isMacOS) const TitleBarActionButtons(), if (!Platform.isMacOS) const TitleBarActionButtons(),
], ],
)), ))
], ],
), ),
), ),

View File

@ -31,7 +31,9 @@ class Sidebar extends HookConsumerWidget {
static void goToSettings(BuildContext context) { static void goToSettings(BuildContext context) {
Navigator.of(context).push(SpotubePageRoute( Navigator.of(context).push(SpotubePageRoute(
child: const Settings(), child: const Settings(
key: Key("settings"),
),
)); ));
} }

View File

@ -70,7 +70,9 @@ class Lyrics extends HookConsumerWidget {
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
Navigator.of(context).push(SpotubePageRoute( Navigator.of(context).push(SpotubePageRoute(
child: const Settings(), child: const Settings(
key: Key("settings"),
),
)); ));
}, },
child: const Text("Add Access Token")) child: const Text("Add Access Token"))

View File

@ -4,13 +4,17 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:shared_preferences/shared_preferences.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/Shared/DownloadTrackButton.dart';
import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/components/Player/PlayerControls.dart';
import 'package:spotube/helpers/artists-to-clickable-artists.dart'; import 'package:spotube/helpers/artists-to-clickable-artists.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/search-youtube.dart'; import 'package:spotube/helpers/search-youtube.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -19,15 +23,17 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
class Player extends HookConsumerWidget { class Player extends HookConsumerWidget {
const Player({Key? key}) : super(key: key); const Player({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider);
final _isPlaying = useState(false); final _isPlaying = useState(false);
final _shuffled = useState(false); final _shuffled = useState(false);
final _volume = useState(0.0); final _volume = useState(0.0);
final _duration = useState<Duration?>(null); final _duration = useState<Duration?>(null);
final _currentTrackId = useState<String?>(null); final _currentTrackId = useState<String?>(null);
final breakpoint = useBreakpoints();
final AudioPlayer player = useMemoized(() => AudioPlayer(), []); final AudioPlayer player = useMemoized(() => AudioPlayer(), []);
final YoutubeExplode youtube = useMemoized(() => YoutubeExplode(), []); final YoutubeExplode youtube = useMemoized(() => YoutubeExplode(), []);
final Future<SharedPreferences> future = final Future<SharedPreferences> future =
@ -92,6 +98,7 @@ class Player extends HookConsumerWidget {
print(stack); print(stack);
} }
}); });
return () { return () {
playingStreamListener.cancel(); playingStreamListener.cancel();
durationStreamListener.cancel(); durationStreamListener.cancel();
@ -106,9 +113,11 @@ class Player extends HookConsumerWidget {
_volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ?? _volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ??
player.volume; player.volume;
} }
return null;
}, [localStorage.data]); }, [localStorage.data]);
var _playTrack = useCallback((Track currentTrack, Playback playback) async { final _playTrack =
useCallback((Track currentTrack, Playback playback) async {
try { try {
if (currentTrack.id != _currentTrackId.value) { if (currentTrack.id != _currentTrackId.value) {
Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? ""); Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? "");
@ -140,6 +149,13 @@ class Player extends HookConsumerWidget {
} }
}, [player, _currentTrackId, _duration]); }, [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 { var _onNext = useCallback(() async {
try { try {
await player.pause(); await player.pause();
@ -162,193 +178,202 @@ class Player extends HookConsumerWidget {
} }
}, [player]); }, [player]);
String albumArt = useMemoized(
() => imageToUrlString(
playback.currentTrack?.album?.images,
index: (playback.currentTrack?.album?.images?.length ?? 1) - 1,
),
[playback.currentTrack?.album?.images],
);
final entryRef = useRef<OverlayEntry?>(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( return Container(
color: Theme.of(context).backgroundColor, color: Theme.of(context).backgroundColor,
child: HookConsumer( child: Material(
builder: (context, ref, widget) { type: MaterialType.transparency,
Playback playback = ref.watch(playbackProvider); child: Row(
if (playback.currentPlaylist != null && mainAxisAlignment: MainAxisAlignment.spaceEvenly,
playback.currentTrack != null) { children: [
_playTrack(playback.currentTrack!, playback); Expanded(child: PlayerTrackDetails(albumArt: albumArt)),
} // controls
Flexible(
String? albumArt = useMemoized( flex: 3,
() => imageToUrlString( child: controls,
playback.currentTrack?.album?.images,
index: (playback.currentTrack?.album?.images?.length ?? 1) - 1,
), ),
[playback.currentTrack?.album?.images], // add to saved tracks
); Expanded(
flex: 1,
return Material( child: Wrap(
type: MaterialType.transparency, alignment: WrapAlignment.center,
child: Row( runAlignment: WrapAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
children: [ Container(
if (albumArt != null) height: 20,
CachedNetworkImage( constraints: const BoxConstraints(maxWidth: 200),
imageUrl: albumArt, child: Slider.adaptive(
maxHeightDiskCache: 50, value: _volume.value,
maxWidthDiskCache: 50, onChanged: (value) async {
placeholder: (context, url) { try {
return Container( await player.setVolume(value).then((_) {
height: 50, _volume.value = value;
width: 50, localStorage.data?.setDouble(
color: Colors.green[400], LocalStorageKeys.volume,
); value,
}, );
), });
// title of the currently playing track } catch (e, stack) {
Flexible( print("[VolumeSlider.onChange()] $e");
flex: 1, print(stack);
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;
} }
} 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);
}
},
), ),
), Row(
// add to saved tracks mainAxisAlignment: MainAxisAlignment.center,
Expanded(
flex: 1,
child: Wrap(
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
children: [ children: [
Container( DownloadTrackButton(
height: 20, track: playback.currentTrack,
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<bool>(
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!);
}
});
});
}),
],
), ),
Consumer(builder: (context, ref, widget) {
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
return FutureBuilder<bool>(
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!);
}
});
});
}),
], ],
), ),
) ],
], ),
), )
); ],
}, ),
), ),
); );
} }

View File

@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:spotube/helpers/zero-pad-num-str.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/models/GlobalKeyActions.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
@ -77,79 +78,91 @@ class PlayerControls extends HookConsumerWidget {
}; };
}); });
return Container( final breakpoint = useBreakpoints();
constraints: const BoxConstraints(maxWidth: 700),
child: Column(
children: [
StreamBuilder<Duration>(
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; Widget controlButtons = Material(
var sliderValue = snapshot.data?.inSeconds ?? 0; type: MaterialType.transparency,
return Row( child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Expanded( children: [
child: Slider.adaptive( if (breakpoint.isMoreThan(Breakpoints.md))
// cannot divide by zero IconButton(
// there's an edge case for value being bigger icon: const Icon(Icons.shuffle_rounded),
// than total duration. Keeping it resolved color: shuffled ? Theme.of(context).primaryColor : null,
value: (sliderMax == 0 || sliderValue > sliderMax) onPressed: () {
? 0 onShuffle?.call();
: sliderValue / sliderMax, }),
onChanged: (value) {}, IconButton(
onChangeEnd: (value) { icon: const Icon(Icons.skip_previous_rounded),
onSeek?.call(value * sliderMax); onPressed: () {
}, onPrevious?.call();
),
),
Text(
"$currentMinutes:$currentSeconds/$totalMinutes:$totalSeconds",
)
],
);
}), }),
Row( IconButton(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, icon: Icon(
children: [ isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded,
IconButton( ),
icon: const Icon(Icons.shuffle_rounded), onPressed: () => _playOrPause(null),
color: shuffled ? Theme.of(context).primaryColor : null, ),
onPressed: () { IconButton(
onShuffle?.call(); icon: const Icon(Icons.skip_next_rounded),
}), onPressed: () => onNext?.call()),
IconButton( if (breakpoint.isMoreThan(Breakpoints.md))
icon: const Icon(Icons.skip_previous_rounded), IconButton(
onPressed: () { icon: const Icon(Icons.stop_rounded),
onPrevious?.call(); 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()),
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<Duration>(
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,
],
));
} }
} }

View File

@ -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<PaletteGenerator>(
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,
],
),
);
}),
);
}
}

View File

@ -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,
)
],
),
),
],
);
}
}

View File

@ -26,9 +26,14 @@ class PlaylistCard extends HookConsumerWidget {
imageUrl: playlist.images![0].url!, imageUrl: playlist.images![0].url!,
isPlaying: isPlaylistPlaying, isPlaying: isPlaylistPlaying,
onTap: () { onTap: () {
Navigator.of(context).push(SpotubePageRoute( Navigator.of(context).push(
child: PlaylistView(playlist), SpotubePageRoute(
)); child: PlaylistView(
playlist,
key: Key("playlist-${playlist.id}"),
),
),
);
}, },
onPlaybuttonPressed: () async { onPlaybuttonPressed: () async {
if (isPlaylistPlaying) return; if (isPlaylistPlaying) return;

View File

@ -4,8 +4,8 @@ class SpotubePageRoute extends PageRouteBuilder {
final Widget child; final Widget child;
SpotubePageRoute({required this.child}) SpotubePageRoute({required this.child})
: super( : super(
pageBuilder: (context, animation, secondaryAnimation) => child, pageBuilder: (context, animation, secondaryAnimation) => child,
); settings: RouteSettings(name: child.key.toString()));
@override @override
Widget buildTransitions(BuildContext context, Animation<double> animation, Widget buildTransitions(BuildContext context, Animation<double> animation,

View File

@ -1,7 +1,9 @@
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:uuid/uuid.dart' show Uuid;
const uuid = Uuid();
String imageToUrlString(List<Image>? images, {int index = 0}) { String imageToUrlString(List<Image>? images, {int index = 0}) {
return images != null && images.isNotEmpty return images != null && images.isNotEmpty
? images[0].url! ? images[0].url!
: "https://avatars.dicebear.com/api/croodles-neutral/${DateTime.now().toString()}.png"; : "https://avatars.dicebear.com/api/croodles-neutral/${uuid.v4()}.png";
} }

View File

@ -226,6 +226,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" 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: hooks_riverpod:
dependency: "direct main" dependency: "direct main"
description: description:
@ -331,6 +338,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -380,6 +394,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" 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: path:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -52,6 +52,8 @@ dependencies:
flutter_riverpod: ^1.0.3 flutter_riverpod: ^1.0.3
flutter_hooks: ^0.18.2+1 flutter_hooks: ^0.18.2+1
hooks_riverpod: ^1.0.3 hooks_riverpod: ^1.0.3
go_router: ^3.0.4
palette_generator: ^0.3.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: