floating mini player works flawlessly

Custom bg-color for floating player for each title track album art
go_true routing integrated
floating player now disappears if not on home
This commit is contained in:
Kingkor Roy Tirtho 2022-03-10 15:11:02 +06:00
parent d608fa7d02
commit aaf74b46d4
19 changed files with 211 additions and 98 deletions

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumView.dart';
@ -32,12 +33,7 @@ class AlbumCard extends HookConsumerWidget {
description:
"Album • ${artistsToString<ArtistSimple>(album.artists ?? [])}",
onTap: () {
Navigator.of(context).push(SpotubePageRoute(
child: AlbumView(
album,
key: Key("album-${album.id}"),
),
));
GoRouter.of(context).push("/album/${album.id}", extra: album);
},
onPlaybuttonPressed: () async {
SpotifyApi spotify = ref.read(spotifyProvider);

View File

@ -1,8 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Artist/ArtistProfile.dart';
import 'package:spotube/components/Shared/SpotubePageRoute.dart';
class ArtistCard extends StatelessWidget {
final Artist artist;
@ -17,12 +16,7 @@ class ArtistCard extends StatelessWidget {
: "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6");
return InkWell(
onTap: () {
Navigator.of(context).push(SpotubePageRoute(
child: ArtistProfile(
artist.id!,
key: Key("artist-${artist.id}"),
),
));
GoRouter.of(context).push("/artist/${artist.id}");
},
borderRadius: BorderRadius.circular(10),
child: Ink(

View File

@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumCard.dart';
@ -243,13 +244,10 @@ class ArtistProfile extends HookConsumerWidget {
TextButton(
child: const Text("See All"),
onPressed: () {
Navigator.of(context).push(SpotubePageRoute(
child: ArtistAlbumView(
artistId,
snapshot.data?.name ?? "KRTX",
key: Key("artist-album-$artistId"),
),
));
GoRouter.of(context).push(
"/artist-album/$artistId",
extra: snapshot.data?.name ?? "KRTX",
);
},
)
],

View File

@ -1,10 +1,9 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/components/Settings.dart';
import 'package:spotube/components/Shared/SpotubePageRoute.dart';
import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/SpotifyDI.dart';
@ -30,11 +29,7 @@ class Sidebar extends HookConsumerWidget {
}
static void goToSettings(BuildContext context) {
Navigator.of(context).push(SpotubePageRoute(
child: const Settings(
key: Key("settings"),
),
));
GoRouter.of(context).push("/settings");
}
@override

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Settings.dart';
@ -69,11 +70,7 @@ class Lyrics extends HookConsumerWidget {
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(SpotubePageRoute(
child: const Settings(
key: Key("settings"),
),
));
GoRouter.of(context).push("/settings");
},
child: const Text("Add Access Token"))
],

View File

@ -1,24 +1,23 @@
import 'dart:async';
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' 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/hooks/usePaletteColor.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/ThemeProvider.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
class Player extends HookConsumerWidget {
@ -26,6 +25,7 @@ class Player extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider);
final _isPlaying = useState(false);
final _shuffled = useState(false);
final _volume = useState(0.0);
@ -200,7 +200,10 @@ class Player extends HookConsumerWidget {
}
}
final paletteColor = usePaletteColor(albumArt);
final controls = PlayerControls(
iconColor: paletteColor.bodyTextColor,
positionStream: player.positionStream,
isPlaying: _isPlaying.value,
duration: _duration.value ?? Duration.zero,
@ -274,6 +277,7 @@ class Player extends HookConsumerWidget {
builder: (context) => PlayerOverlay(
controls: controls,
albumArt: albumArt,
paletteColor: paletteColor,
),
);
// I can't believe useEffect doesn't run Post Frame aka

View File

@ -21,6 +21,7 @@ class PlayerControls extends HookConsumerWidget {
final Function? onPrevious;
final Function? onPlay;
final Function? onPause;
final Color? iconColor;
const PlayerControls({
required this.positionStream,
required this.isPlaying,
@ -33,6 +34,7 @@ class PlayerControls extends HookConsumerWidget {
this.onPrevious,
this.onPlay,
this.onPause,
this.iconColor,
Key? key,
}) : super(key: key);
@ -94,6 +96,7 @@ class PlayerControls extends HookConsumerWidget {
}),
IconButton(
icon: const Icon(Icons.skip_previous_rounded),
color: iconColor,
onPressed: () {
onPrevious?.call();
}),
@ -101,11 +104,14 @@ class PlayerControls extends HookConsumerWidget {
icon: Icon(
isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded,
),
color: iconColor,
onPressed: () => _playOrPause(null),
),
IconButton(
icon: const Icon(Icons.skip_next_rounded),
onPressed: () => onNext?.call()),
icon: const Icon(Icons.skip_next_rounded),
onPressed: () => onNext?.call(),
color: iconColor,
),
if (breakpoint.isMoreThan(Breakpoints.md))
IconButton(
icon: const Icon(Icons.stop_rounded),

View File

@ -1,6 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:spotube/components/Player/PlayerTrackDetails.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
@ -8,48 +8,57 @@ import 'package:spotube/hooks/useBreakpoints.dart';
class PlayerOverlay extends HookWidget {
final Widget controls;
final String albumArt;
final PaletteColor paletteColor;
const PlayerOverlay({
required this.controls,
required this.albumArt,
required this.paletteColor,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final breakpoint = useBreakpoints();
final isCurrentRoute = useState<bool?>(null);
useEffect(() {
WidgetsBinding.instance?.addPostFrameCallback((timer) {
final matches = GoRouter.of(context).location == "/";
if (matches != isCurrentRoute.value) {
isCurrentRoute.value = matches;
}
});
return null;
});
if (isCurrentRoute.value == false) {
return Container();
}
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,
child: Container(
width: MediaQuery.of(context).size.width,
height: 50,
decoration: BoxDecoration(
color: paletteColor.color,
borderRadius: BorderRadius.circular(5),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: PlayerTrackDetails(
albumArt: albumArt,
color: paletteColor.bodyTextColor,
),
),
),
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,
],
),
);
}),
Expanded(child: controls),
],
),
),
);
}
}

View File

@ -7,7 +7,9 @@ import 'package:spotube/provider/Playback.dart';
class PlayerTrackDetails extends HookConsumerWidget {
final String? albumArt;
const PlayerTrackDetails({Key? key, this.albumArt}) : super(key: key);
final Color? color;
const PlayerTrackDetails({Key? key, this.albumArt, this.color})
: super(key: key);
@override
Widget build(BuildContext context, ref) {
@ -36,13 +38,15 @@ class PlayerTrackDetails extends HookConsumerWidget {
),
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),
Flexible(
child: Text(
playback.currentTrack?.name ?? "Not playing",
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodyText1
?.copyWith(fontWeight: FontWeight.bold, color: color),
),
),
],
// title of the currently playing track
@ -53,10 +57,11 @@ class PlayerTrackDetails extends HookConsumerWidget {
children: [
Text(
playback.currentTrack?.name ?? "Not playing",
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodyText1
?.copyWith(fontWeight: FontWeight.bold),
?.copyWith(fontWeight: FontWeight.bold, color: color),
),
artistsToClickableArtists(
playback.currentTrack?.artists ?? [],

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Playlist/PlaylistView.dart';
@ -26,13 +27,9 @@ class PlaylistCard extends HookConsumerWidget {
imageUrl: playlist.images![0].url!,
isPlaying: isPlaylistPlaying,
onTap: () {
Navigator.of(context).push(
SpotubePageRoute(
child: PlaylistView(
playlist,
key: Key("playlist-${playlist.id}"),
),
),
GoRouter.of(context).push(
"/playlist/${playlist.id}",
extra: playlist,
);
},
onPlaybuttonPressed: () async {

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Settings/SettingsHotkeyTile.dart';
@ -57,7 +58,7 @@ class Settings extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: geniusAccessToken != null
onPressed: geniusAccessToken.value != null
? () async {
SharedPreferences localStorage =
await SharedPreferences.getInstance();
@ -148,7 +149,7 @@ class Settings extends HookConsumerWidget {
await SharedPreferences.getInstance();
await localStorage.clear();
auth.logout();
Navigator.of(context).pop();
GoRouter.of(context).pop();
},
),
],

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:spotube/components/Shared/AnchorButton.dart';
class LinkText<T> extends StatelessWidget {
@ -6,12 +7,14 @@ class LinkText<T> extends StatelessWidget {
final TextStyle style;
final TextAlign? textAlign;
final TextOverflow? overflow;
final Route<T> route;
final String route;
final T? extra;
const LinkText(
this.text,
this.route, {
Key? key,
this.textAlign,
this.extra,
this.overflow,
this.style = const TextStyle(),
}) : super(key: key);
@ -20,8 +23,8 @@ class LinkText<T> extends StatelessWidget {
Widget build(BuildContext context) {
return AnchorButton(
text,
onTap: () async {
await Navigator.of(context).push(route);
onTap: () {
GoRouter.of(context).push(route, extra: extra);
},
key: key,
overflow: overflow,

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
class RecordHotKeyDialog extends HookWidget {
@ -66,7 +67,7 @@ class RecordHotKeyDialog extends HookWidget {
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
GoRouter.of(context).pop();
},
),
TextButton(
@ -75,7 +76,7 @@ class RecordHotKeyDialog extends HookWidget {
? null
: () {
onHotKeyRecorded(_hotKey.value);
Navigator.of(context).pop();
GoRouter.of(context).pop();
},
),
],

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class SpotubePageRoute extends PageRouteBuilder {
final Widget child;
@ -16,3 +17,20 @@ class SpotubePageRoute extends PageRouteBuilder {
);
}
}
class SpotubePage extends CustomTransitionPage {
SpotubePage({
required Widget child,
}) : super(
child: child,
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
);
}

View File

@ -180,9 +180,8 @@ class TrackTile extends HookWidget {
Expanded(
child: LinkText(
track.value.album!.name!,
SpotubePageRoute(
child: AlbumView(track.value.album!),
),
"/album/${track.value.album?.id}",
extra: track.value.album,
overflow: TextOverflow.ellipsis,
),
),

View File

@ -21,9 +21,7 @@ Widget artistsToClickableArtists(
(artist.key != artists.length - 1)
? "${artist.value.name}, "
: artist.value.name!,
SpotubePageRoute(
child: ArtistProfile(artist.value.id!),
),
"/artist/${artist.value.id}",
overflow: TextOverflow.ellipsis,
style: textStyle,
),

View File

@ -5,5 +5,5 @@ const uuid = Uuid();
String imageToUrlString(List<Image>? images, {int index = 0}) {
return images != null && images.isNotEmpty
? images[0].url!
: "https://avatars.dicebear.com/api/croodles-neutral/${uuid.v4()}.png";
: "https://avatars.dicebear.com/api/bottts/${uuid.v4()}.png";
}

View File

@ -0,0 +1,32 @@
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';
PaletteColor usePaletteColor(String imageUrl) {
final paletteColor =
useState<PaletteColor>(PaletteColor(Colors.grey[300]!, 0));
final context = useContext();
useEffect(() {
PaletteGenerator.fromImageProvider(
CachedNetworkImageProvider(
imageUrl,
cacheKey: imageUrl,
maxHeight: 50,
maxWidth: 50,
),
).then((palette) {
final color = Theme.of(context).brightness == Brightness.light
? palette.lightMutedColor ?? palette.lightVibrantColor
: palette.darkMutedColor ?? palette.darkVibrantColor;
if (color != null) {
paletteColor.value = color;
}
});
return null;
}, [imageUrl]);
return paletteColor.value;
}

View File

@ -3,10 +3,18 @@ import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumView.dart';
import 'package:spotube/components/Artist/ArtistAlbumView.dart';
import 'package:spotube/components/Artist/ArtistProfile.dart';
import 'package:spotube/components/Home/Home.dart';
import 'package:spotube/components/Playlist/PlaylistView.dart';
import 'package:spotube/components/Settings.dart';
import 'package:spotube/components/Shared/SpotubePageRoute.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/ThemeProvider.dart';
@ -25,6 +33,56 @@ void main() async {
}
class MyApp extends HookConsumerWidget {
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: "/",
builder: (context, state) => const Home(),
),
GoRoute(
path: "/settings",
pageBuilder: (context, state) => SpotubePage(
child: const Settings(),
),
),
GoRoute(
path: "/album/:id",
pageBuilder: (context, state) {
assert(state.extra is AlbumSimple);
return SpotubePage(child: AlbumView(state.extra as AlbumSimple));
},
),
GoRoute(
path: "/artist/:id",
pageBuilder: (context, state) {
assert(state.params["id"] != null);
return SpotubePage(child: ArtistProfile(state.params["id"]!));
},
),
GoRoute(
path: "/artist-album/:id",
pageBuilder: (context, state) {
assert(state.params["id"] != null);
assert(state.extra is String);
return SpotubePage(
child: ArtistAlbumView(
state.params["id"]!,
state.extra as String,
),
);
},
),
GoRoute(
path: "/playlist/:id",
pageBuilder: (context, state) {
assert(state.extra is PlaylistSimple);
return SpotubePage(
child: PlaylistView(state.extra as PlaylistSimple),
);
},
),
],
);
@override
Widget build(BuildContext context, ref) {
var themeMode = ref.watch(themeProvider);
@ -44,9 +102,12 @@ class MyApp extends HookConsumerWidget {
themeNotifier.state = ThemeMode.system;
}
});
return null;
}, []);
return MaterialApp(
return MaterialApp.router(
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
debugShowCheckedModeBanner: false,
title: 'Spotube',
theme: ThemeData(
@ -142,7 +203,6 @@ class MyApp extends HookConsumerWidget {
canvasColor: Colors.blueGrey[900],
),
themeMode: themeMode,
home: const Home(),
);
}
}