diff --git a/lib/components/Album/AlbumCard.dart b/lib/components/Album/AlbumCard.dart index b12b9687..0e479fcc 100644 --- a/lib/components/Album/AlbumCard.dart +++ b/lib/components/Album/AlbumCard.dart @@ -1,5 +1,4 @@ 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/Shared/PlaybuttonCard.dart'; @@ -7,6 +6,7 @@ import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class AlbumCard extends HookConsumerWidget { @@ -33,7 +33,7 @@ class AlbumCard extends HookConsumerWidget { description: "Album • ${TypeConversionUtils.artists_X_String(album.artists ?? [])}", onTap: () { - GoRouter.of(context).push("/album/${album.id}", extra: album); + ServiceUtils.navigate(context, "/album/${album.id}", extra: album); }, onPlaybuttonPressed: () async { SpotifyApi spotify = ref.read(spotifyProvider); diff --git a/lib/components/Artist/ArtistCard.dart b/lib/components/Artist/ArtistCard.dart index cf13be86..8619fee1 100644 --- a/lib/components/Artist/ArtistCard.dart +++ b/lib/components/Artist/ArtistCard.dart @@ -1,9 +1,9 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Shared/HoverBuilder.dart'; import 'package:spotube/components/Shared/UniversalImage.dart'; +import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class ArtistCard extends StatelessWidget { @@ -23,7 +23,7 @@ class ArtistCard extends StatelessWidget { width: 200, child: InkWell( onTap: () { - GoRouter.of(context).push("/artist/${artist.id}"); + ServiceUtils.navigate(context, "/artist/${artist.id}"); }, borderRadius: BorderRadius.circular(10), child: HoverBuilder(builder: (context, isHovering) { diff --git a/lib/components/Home/Genres.dart b/lib/components/Home/Genres.dart new file mode 100644 index 00000000..4b85d4d9 --- /dev/null +++ b/lib/components/Home/Genres.dart @@ -0,0 +1,64 @@ +import 'package:fl_query_hooks/fl_query_hooks.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Category/CategoryCard.dart'; +import 'package:spotube/components/LoaderShimmers/ShimmerCategories.dart'; +import 'package:spotube/components/Shared/Waypoint.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; +import 'package:spotube/provider/UserPreferences.dart'; + +class Genres extends HookConsumerWidget { + const Genres({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final spotify = ref.watch(spotifyProvider); + final recommendationMarket = ref.watch( + userPreferencesProvider.select((s) => s.recommendationMarket), + ); + final categoriesQuery = useInfiniteQuery( + job: categoriesQueryJob, + externalData: { + "spotify": spotify, + "recommendationMarket": recommendationMarket, + }, + ); + final categories = [ + useMemoized( + () => Category() + ..id = "user-featured-playlists" + ..name = "Featured", + [], + ), + ...categoriesQuery.pages + .expand( + (page) => page?.items ?? const Iterable.empty(), + ) + .toList() + ]; + + return Scaffold( + body: ListView.builder( + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + if (category == null) return Container(); + if (index == categories.length - 1) { + return Waypoint( + onEnter: () { + if (categoriesQuery.hasNextPage) { + categoriesQuery.fetchNextPage(); + } + }, + child: const ShimmerCategories(), + ); + } + return CategoryCard(category); + }, + ), + ); + } +} diff --git a/lib/components/Home/Shell.dart b/lib/components/Home/Shell.dart new file mode 100644 index 00000000..4c39b6cc --- /dev/null +++ b/lib/components/Home/Shell.dart @@ -0,0 +1,106 @@ +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:spotube/components/Home/Sidebar.dart'; +import 'package:spotube/components/Home/SpotubeNavigationBar.dart'; +import 'package:spotube/components/Player/Player.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; +import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart'; +import 'package:spotube/hooks/useUpdateChecker.dart'; +import 'package:spotube/provider/Downloader.dart'; + +const _path = { + 0: "/", + 1: "/search", + 2: "/library", + 3: "/lyrics", +}; + +class Shell extends HookConsumerWidget { + final Widget child; + const Shell({ + required this.child, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final index = useState(0); + final isMounted = useIsMounted(); + + final downloader = ref.watch(downloaderProvider); + useEffect(() { + downloader.onFileExists = (track) async { + if (!isMounted()) return false; + return await showDialog( + context: context, + builder: (context) => ReplaceDownloadedFileDialog( + track: track, + ), + ) ?? + false; + }; + return null; + }, [downloader]); + + // checks for latest version of the application + useUpdateChecker(ref); + + final backgroundColor = Theme.of(context).backgroundColor; + + useEffect(() { + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( + statusBarColor: backgroundColor, // status bar color + statusBarIconBrightness: backgroundColor.computeLuminance() > 0.179 + ? Brightness.dark + : Brightness.light, + ), + ); + return null; + }, [backgroundColor]); + + const pageWindowTitleBar = PageWindowTitleBar(); + return Scaffold( + primary: true, + appBar: _path.values.contains(GoRouter.of(context).location) + ? pageWindowTitleBar + : null, + extendBodyBehindAppBar: true, + body: Row( + children: [ + Sidebar( + selectedIndex: index.value, + onSelectedIndexChanged: (selectedIndex) { + index.value = selectedIndex; + GoRouter.of(context).go(_path[selectedIndex]!); + }, + ), + Expanded( + child: Column( + children: [ + Expanded(child: child), + ], + ), + ), + ], + ), + extendBody: true, + bottomNavigationBar: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Player(), + SpotubeNavigationBar( + selectedIndex: index.value, + onSelectedIndexChanged: (selectedIndex) { + index.value = selectedIndex; + GoRouter.of(context).go(_path[selectedIndex]!); + }, + ), + ], + ), + ); + } +} diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index ed55873c..9f9db900 100644 --- a/lib/components/Home/Sidebar.dart +++ b/lib/components/Home/Sidebar.dart @@ -1,7 +1,6 @@ import 'package:badges/badges.dart'; import 'package:bitsdojo_window/bitsdojo_window.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:spotube/components/Shared/UniversalImage.dart'; @@ -12,6 +11,7 @@ import 'package:spotube/provider/Downloader.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/utils/platform.dart'; +import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; final sidebarExtendedStateProvider = StateProvider((ref) => null); @@ -35,7 +35,7 @@ class Sidebar extends HookConsumerWidget { } static void goToSettings(BuildContext context) { - GoRouter.of(context).push("/settings"); + ServiceUtils.navigate(context, "/settings"); } @override @@ -78,46 +78,52 @@ class Sidebar extends HookConsumerWidget { !(forceExtended ?? extended.value); return SafeArea( + top: false, child: Material( color: Theme.of(context).navigationRailTheme.backgroundColor, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (selectedIndex == 3 && kIsDesktop) + if (kIsDesktop) SizedBox( height: appWindow.titleBarHeight, width: extended.value ? 256 : 80, - child: MoveWindow(), + child: MoveWindow( + child: !extended.value + ? Center( + child: IconButton( + icon: const Icon(Icons.menu_rounded), + onPressed: toggleExtended, + ), + ) + : null, + ), ), - Padding( - padding: const EdgeInsets.only(left: 10), - child: (extended.value) - ? Row( - children: [ - _buildSmallLogo(), - const SizedBox( - width: 10, - ), - Text( - "Spotube", - style: Theme.of(context).textTheme.headline4, - ), - IconButton( - icon: const Icon(Icons.menu_rounded), - onPressed: toggleExtended, - ), - ], - ) - : Column( - children: [ - IconButton( - icon: const Icon(Icons.menu_rounded), - onPressed: toggleExtended, - ), - _buildSmallLogo(), - ], - ), - ), + if (!kIsDesktop && !extended.value) + Center( + child: IconButton( + icon: const Icon(Icons.menu_rounded), + onPressed: toggleExtended, + ), + ), + (extended.value) + ? Row( + children: [ + _buildSmallLogo(), + const SizedBox( + width: 10, + ), + Text( + "Spotube", + style: Theme.of(context).textTheme.headline4, + ), + IconButton( + icon: const Icon(Icons.menu_rounded), + onPressed: toggleExtended, + ), + ], + ) + : _buildSmallLogo(), Expanded( child: NavigationRail( destinations: sidebarTileList.map( @@ -166,7 +172,7 @@ class Sidebar extends HookConsumerWidget { ); if (extended.value) { return Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16).copyWith(left: 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/components/Library/UserLibrary.dart b/lib/components/Library/UserLibrary.dart index 9d504793..26ef766a 100644 --- a/lib/components/Library/UserLibrary.dart +++ b/lib/components/Library/UserLibrary.dart @@ -12,32 +12,30 @@ class UserLibrary extends ConsumerWidget { const UserLibrary({Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { - return Expanded( - child: DefaultTabController( - length: 5, - child: SafeArea( - child: Scaffold( - appBar: ColoredTabBar( - color: Theme.of(context).backgroundColor, - child: const TabBar( - isScrollable: true, - tabs: [ - Tab(text: "Playlist"), - Tab(text: "Downloads"), - Tab(text: "Local"), - Tab(text: "Artists"), - Tab(text: "Album"), - ], - ), + return DefaultTabController( + length: 5, + child: SafeArea( + child: Scaffold( + appBar: ColoredTabBar( + color: Theme.of(context).backgroundColor, + child: const TabBar( + isScrollable: true, + tabs: [ + Tab(text: "Playlist"), + Tab(text: "Downloads"), + Tab(text: "Local"), + Tab(text: "Artists"), + Tab(text: "Album"), + ], ), - body: const TabBarView(children: [ - AnonymousFallback(child: UserPlaylists()), - UserDownloads(), - UserLocalTracks(), - AnonymousFallback(child: UserArtists()), - AnonymousFallback(child: UserAlbums()), - ]), ), + body: const TabBarView(children: [ + AnonymousFallback(child: UserPlaylists()), + UserDownloads(), + UserLocalTracks(), + AnonymousFallback(child: UserArtists()), + AnonymousFallback(child: UserAlbums()), + ]), ), ), ); diff --git a/lib/components/Login/LoginTutorial.dart b/lib/components/Login/LoginTutorial.dart index f217e52f..21a51bbb 100644 --- a/lib/components/Login/LoginTutorial.dart +++ b/lib/components/Login/LoginTutorial.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; import 'package:introduction_screen/introduction_screen.dart'; import 'package:spotube/components/Login/TokenLoginForms.dart'; import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/provider/Auth.dart'; +import 'package:spotube/utils/service_utils.dart'; class LoginTutorial extends ConsumerWidget { const LoginTutorial({Key? key}) : super(key: key); @@ -30,7 +30,7 @@ class LoginTutorial extends ConsumerWidget { overrideDone: TextButton( onPressed: auth.isLoggedIn ? () { - GoRouter.of(context).go("/"); + ServiceUtils.navigate(context, "/"); } : null, child: const Text("Done"), diff --git a/lib/components/Login/TokenLogin.dart b/lib/components/Login/TokenLogin.dart index 57b2be9a..16c0dea9 100644 --- a/lib/components/Login/TokenLogin.dart +++ b/lib/components/Login/TokenLogin.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/components/Login/TokenLoginForms.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/utils/service_utils.dart'; class TokenLogin extends HookConsumerWidget { const TokenLogin({Key? key}) : super(key: key); @@ -37,7 +38,7 @@ class TokenLogin extends HookConsumerWidget { ), const SizedBox(height: 10), TokenLoginForm( - onDone: () => GoRouter.of(context).go("/"), + onDone: () => ServiceUtils.navigate(context, "/"), ), const SizedBox(height: 10), Wrap( diff --git a/lib/components/Login/WebViewLogin.dart b/lib/components/Login/WebViewLogin.dart index a379c8c3..723b4f97 100644 --- a/lib/components/Login/WebViewLogin.dart +++ b/lib/components/Login/WebViewLogin.dart @@ -61,7 +61,7 @@ class WebViewLogin extends HookConsumerWidget { expiration: body.expiration, ); if (mounted()) { - GoRouter.of(context).go("/"); + ServiceUtils.navigate(context, "/"); } } }, diff --git a/lib/components/Lyrics/Lyrics.dart b/lib/components/Lyrics/Lyrics.dart index 15bc3415..a699592a 100644 --- a/lib/components/Lyrics/Lyrics.dart +++ b/lib/components/Lyrics/Lyrics.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart'; -import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; @@ -25,7 +24,6 @@ class Lyrics extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - PageWindowTitleBar(foregroundColor: titleBarForegroundColor), Center( child: Text( playback.track?.name ?? "", diff --git a/lib/components/Lyrics/SyncedLyrics.dart b/lib/components/Lyrics/SyncedLyrics.dart index eebe336f..2b774b9f 100644 --- a/lib/components/Lyrics/SyncedLyrics.dart +++ b/lib/components/Lyrics/SyncedLyrics.dart @@ -1,7 +1,5 @@ import 'dart:ui'; -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -9,7 +7,6 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart'; import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart'; import 'package:spotube/components/Lyrics/Lyrics.dart'; -import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/SpotubeMarqueeText.dart'; import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/hooks/useAutoScrollController.dart'; @@ -129,130 +126,125 @@ class SyncedLyrics extends HookConsumerWidget { noSetBGColor: true, ); - return Expanded( - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - image: DecorationImage( - image: UniversalImage.imageProvider(albumArt), - fit: BoxFit.cover, - ), + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + image: DecorationImage( + image: UniversalImage.imageProvider(albumArt), + fit: BoxFit.cover, ), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: Container( - color: palette.color.withOpacity(.7), - child: SafeArea( - child: failed.value - ? Lyrics(titleBarForegroundColor: palette.bodyTextColor) - : Column( - children: [ - PageWindowTitleBar( - foregroundColor: palette.bodyTextColor, - ), - SizedBox( - height: breakpoint >= Breakpoints.md ? 50 : 30, - child: Material( - type: MaterialType.transparency, - child: Stack( - children: [ - Center( - child: SpotubeMarqueeText( - text: playback.track?.name ?? "Not Playing", - style: headlineTextStyle, - isHovering: true, + ), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container( + color: palette.color.withOpacity(.7), + child: SafeArea( + child: failed.value + ? Lyrics(titleBarForegroundColor: palette.bodyTextColor) + : Column( + children: [ + SizedBox( + height: breakpoint >= Breakpoints.md ? 50 : 30, + child: Material( + type: MaterialType.transparency, + child: Stack( + children: [ + Center( + child: SpotubeMarqueeText( + text: playback.track?.name ?? "Not Playing", + style: headlineTextStyle, + isHovering: true, + ), + ), + Positioned.fill( + child: Align( + alignment: Alignment.centerRight, + child: IconButton( + tooltip: "Lyrics Delay", + icon: const Icon(Icons.av_timer_rounded), + onPressed: () async { + final delay = await showDialog( + context: context, + builder: (context) => + const LyricDelayAdjustDialog(), + ); + if (delay != null) { + ref + .read(lyricDelayState.notifier) + .state = delay; + } + }, ), ), - Positioned.fill( - child: Align( - alignment: Alignment.centerRight, - child: IconButton( - tooltip: "Lyrics Delay", - icon: const Icon(Icons.av_timer_rounded), - onPressed: () async { - final delay = await showDialog( - context: context, - builder: (context) => - const LyricDelayAdjustDialog(), - ); - if (delay != null) { - ref - .read(lyricDelayState.notifier) - .state = delay; - } - }, - ), - ), - ), - ], - ), + ), + ], ), ), - Center( - child: Text( - TypeConversionUtils.artists_X_String( - playback.track?.artists ?? []), - style: breakpoint >= Breakpoints.md - ? textTheme.headline5 - : textTheme.headline6, - ), + ), + Center( + child: Text( + TypeConversionUtils.artists_X_String( + playback.track?.artists ?? []), + style: breakpoint >= Breakpoints.md + ? textTheme.headline5 + : textTheme.headline6, ), - if (lyricValue != null && lyricValue.lyrics.isNotEmpty) - Expanded( - child: ListView.builder( - controller: controller, - itemCount: lyricValue.lyrics.length, - itemBuilder: (context, index) { - final lyricSlice = lyricValue.lyrics[index]; - final isActive = - lyricSlice.time.inSeconds == currentTime; + ), + if (lyricValue != null && lyricValue.lyrics.isNotEmpty) + Expanded( + child: ListView.builder( + controller: controller, + itemCount: lyricValue.lyrics.length, + itemBuilder: (context, index) { + final lyricSlice = lyricValue.lyrics[index]; + final isActive = + lyricSlice.time.inSeconds == currentTime; - if (isActive) { - controller.scrollToIndex( - index, - preferPosition: AutoScrollPosition.middle, - ); - } - return AutoScrollTag( - key: ValueKey(index), - index: index, - controller: controller, - child: lyricSlice.text.isEmpty - ? Container() - : Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: AnimatedDefaultTextStyle( - duration: const Duration( - milliseconds: 250), - style: TextStyle( - color: isActive - ? Colors.white - : palette.bodyTextColor, - fontWeight: isActive - ? FontWeight.bold - : FontWeight.normal, - fontSize: isActive ? 30 : 26, - ), - child: Text( - lyricSlice.text, - maxLines: 2, - textAlign: TextAlign.center, - ), + if (isActive) { + controller.scrollToIndex( + index, + preferPosition: AutoScrollPosition.middle, + ); + } + return AutoScrollTag( + key: ValueKey(index), + index: index, + controller: controller, + child: lyricSlice.text.isEmpty + ? Container() + : Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedDefaultTextStyle( + duration: const Duration( + milliseconds: 250), + style: TextStyle( + color: isActive + ? Colors.white + : palette.bodyTextColor, + fontWeight: isActive + ? FontWeight.bold + : FontWeight.normal, + fontSize: isActive ? 30 : 26, + ), + child: Text( + lyricSlice.text, + maxLines: 2, + textAlign: TextAlign.center, ), ), ), - ); - }, - ), + ), + ); + }, ), - if (playback.track != null && - (lyricValue == null || - lyricValue.lyrics.isEmpty == true)) - const Expanded(child: ShimmerLyrics()), - ], - ), - ), + ), + if (playback.track != null && + (lyricValue == null || + lyricValue.lyrics.isEmpty == true)) + const Expanded(child: ShimmerLyrics()), + ], + ), ), ), ), diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index c9c55597..f012d9de 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -35,60 +35,15 @@ class Player extends HookConsumerWidget { [playback.track?.album?.images], ); - final entryRef = useRef(null); - - void disposeOverlay() { - try { - entryRef.value?.remove(); - entryRef.value = null; - } catch (e, stack) { - if (e is! AssertionError) { - logger.e("useEffect.cleanup", e, stack); - } - } - } - - useEffect(() { - // 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) { - // clearing the overlay-entry as passing the already available - // entry will result in splashing while resizing the window - if ((layoutMode == LayoutMode.compact || - (breakpoint.isLessThanOrEqualTo(Breakpoints.md) && - layoutMode == LayoutMode.adaptive)) && - entryRef.value == null && - playback.track != null) { - entryRef.value = OverlayEntry( - opaque: false, - builder: (context) => PlayerOverlay(albumArt: albumArt), - ); - try { - Overlay.of(context)?.insert(entryRef.value!); - } catch (e) { - if (e is AssertionError && - e.message == - 'The specified entry is already present in the Overlay.') { - disposeOverlay(); - Overlay.of(context)?.insert(entryRef.value!); - } - } - } else { - disposeOverlay(); - } - }); - return () { - disposeOverlay(); - }; - }, [breakpoint, playback.track, layoutMode]); - // returning an empty non spacious Container as the overlay will take // place in the global overlay stack aka [_entries] if (layoutMode == LayoutMode.compact || (breakpoint.isLessThanOrEqualTo(Breakpoints.md) && layoutMode == LayoutMode.adaptive)) { - return Container(); + return Padding( + padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8, top: 0), + child: PlayerOverlay(albumArt: albumArt), + ); } return Container( diff --git a/lib/components/Player/PlayerOverlay.dart b/lib/components/Player/PlayerOverlay.dart index ee9e2c3b..0af1b362 100644 --- a/lib/components/Player/PlayerOverlay.dart +++ b/lib/components/Player/PlayerOverlay.dart @@ -5,11 +5,10 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/components/Player/PlayerTrackDetails.dart'; import 'package:spotube/hooks/playback.dart'; -import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/models/Intents.dart'; import 'package:spotube/provider/Playback.dart'; -import 'package:spotube/provider/UserPreferences.dart'; +import 'package:spotube/utils/service_utils.dart'; class PlayerOverlay extends HookConsumerWidget { final String albumArt; @@ -21,110 +20,84 @@ class PlayerOverlay extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final breakpoint = useBreakpoints(); final paletteColor = usePaletteColor(albumArt, ref); - final layoutMode = ref.watch( - userPreferencesProvider.select((s) => s.layoutMode), - ); - - var isHome = GoRouter.of(context).location == "/"; - final isAllowedPage = ["/playlist/", "/album/"].any( - (el) => GoRouter.of(context).location.startsWith(el), - ); final onNext = useNextTrack(ref); final onPrevious = usePreviousTrack(ref); - if (!isHome && !isAllowedPage) return Container(); - - return AnimatedPositioned( - duration: const Duration(milliseconds: 2500), - right: (breakpoint.isMd && !isAllowedPage ? 10 : 5), - left: (layoutMode == LayoutMode.compact || - (breakpoint.isSm && layoutMode == LayoutMode.adaptive) || - isAllowedPage - ? 5 - : 90), - bottom: (layoutMode == LayoutMode.compact && !isAllowedPage) || - (breakpoint.isSm && - layoutMode == LayoutMode.adaptive && - !isAllowedPage) - ? 63 - : 10, - child: GestureDetector( - onVerticalDragEnd: (details) { - int sensitivity = 8; - if (details.primaryVelocity != null && - details.primaryVelocity! < -sensitivity) { - GoRouter.of(context).push("/player"); - } - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(5), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - width: MediaQuery.of(context).size.width, - height: 50, - decoration: BoxDecoration( - color: paletteColor.color.withOpacity(.7), - border: Border.all( - color: paletteColor.titleTextColor, - width: 2, - ), - borderRadius: BorderRadius.circular(5), + return GestureDetector( + onVerticalDragEnd: (details) { + int sensitivity = 8; + if (details.primaryVelocity != null && + details.primaryVelocity! < -sensitivity) { + ServiceUtils.navigate(context, "/player"); + } + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + width: MediaQuery.of(context).size.width, + height: 50, + decoration: BoxDecoration( + color: paletteColor.color.withOpacity(.7), + border: Border.all( + color: paletteColor.titleTextColor, + width: 2, ), - child: Material( - type: MaterialType.transparency, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => GoRouter.of(context).push("/player"), - child: PlayerTrackDetails( - albumArt: albumArt, - color: paletteColor.bodyTextColor, - ), + borderRadius: BorderRadius.circular(5), + ), + child: Material( + type: MaterialType.transparency, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => GoRouter.of(context).push("/player"), + child: PlayerTrackDetails( + albumArt: albumArt, + color: paletteColor.bodyTextColor, ), ), ), - Row( - children: [ - IconButton( - icon: const Icon(Icons.skip_previous_rounded), - color: paletteColor.bodyTextColor, - onPressed: () { - onPrevious(); - }), - Consumer( - builder: (context, ref, _) { - return IconButton( - icon: Icon( - ref.read(playbackProvider).isPlaying - ? Icons.pause_rounded - : Icons.play_arrow_rounded, - ), - color: paletteColor.bodyTextColor, - onPressed: Actions.handler( - context, - PlayPauseIntent(ref), - ), - ); - }, - ), - IconButton( - icon: const Icon(Icons.skip_next_rounded), - onPressed: () => onNext(), + ), + Row( + children: [ + IconButton( + icon: const Icon(Icons.skip_previous_rounded), color: paletteColor.bodyTextColor, - ), - ], - ), - ], - ), + onPressed: () { + onPrevious(); + }), + Consumer( + builder: (context, ref, _) { + return IconButton( + icon: Icon( + ref.read(playbackProvider).isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + ), + color: paletteColor.bodyTextColor, + onPressed: Actions.handler( + context, + PlayPauseIntent(ref), + ), + ); + }, + ), + IconButton( + icon: const Icon(Icons.skip_next_rounded), + onPressed: () => onNext(), + color: paletteColor.bodyTextColor, + ), + ], + ), + ], ), ), ), diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index 7827f51c..e192225d 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -7,6 +7,7 @@ import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class PlaylistCard extends HookConsumerWidget { @@ -30,7 +31,8 @@ class PlaylistCard extends HookConsumerWidget { isPlaying: isPlaylistPlaying && playback.isPlaying, isLoading: playback.status == PlaybackStatus.loading && isPlaylistPlaying, onTap: () { - GoRouter.of(context).push( + ServiceUtils.navigate( + context, "/playlist/${playlist.id}", extra: playlist, ); diff --git a/lib/components/Playlist/PlaylistGenreView.dart b/lib/components/Playlist/PlaylistGenreView.dart index 647b517e..aa519f2c 100644 --- a/lib/components/Playlist/PlaylistGenreView.dart +++ b/lib/components/Playlist/PlaylistGenreView.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/provider/SpotifyDI.dart'; class PlaylistGenreView extends ConsumerWidget { diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index 1e019c21..10c3698b 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -33,231 +33,223 @@ class Search extends HookConsumerWidget { final breakpoint = useBreakpoints(); if (auth.isAnonymous) { - return const Expanded(child: AnonymousFallback()); + return const AnonymousFallback(); } final searchSnapshot = ref.watch(searchQuery(searchTerm)); - return Expanded( - child: SafeArea( - child: Material( - color: Theme.of(context).backgroundColor, - child: Column( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 10, - ), - color: Theme.of(context).backgroundColor, - child: TextField( - controller: controller, - decoration: InputDecoration( - isDense: true, - suffix: ElevatedButton( - child: const Icon(Icons.search_rounded), - onPressed: () { - ref.read(searchTermStateProvider.notifier).state = - controller.value.text; - }, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 7, - ), - hintStyle: const TextStyle(height: 2), - hintText: "Search...", - ), - onSubmitted: (value) { - ref.read(searchTermStateProvider.notifier).state = - controller.value.text; - }, - ), + return SafeArea( + child: Material( + color: Theme.of(context).backgroundColor, + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, ), - searchSnapshot.when( - data: (data) { - Playback playback = ref.watch(playbackProvider); - List albums = []; - List artists = []; - List tracks = []; - List playlists = []; - for (MapEntry page in data.asMap().entries) { - for (var item in page.value.items ?? []) { - if (item is AlbumSimple) { - albums.add(item); - } else if (item is PlaylistSimple) { - playlists.add(item); - } else if (item is Artist) { - artists.add(item); - } else if (item is Track) { - tracks.add(item); - } + color: Theme.of(context).backgroundColor, + child: TextField( + controller: controller, + decoration: InputDecoration( + isDense: true, + suffix: ElevatedButton( + child: const Icon(Icons.search_rounded), + onPressed: () { + ref.read(searchTermStateProvider.notifier).state = + controller.value.text; + }, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 7, + ), + hintStyle: const TextStyle(height: 2), + hintText: "Search...", + ), + onSubmitted: (value) { + ref.read(searchTermStateProvider.notifier).state = + controller.value.text; + }, + ), + ), + searchSnapshot.when( + data: (data) { + Playback playback = ref.watch(playbackProvider); + List albums = []; + List artists = []; + List tracks = []; + List playlists = []; + for (MapEntry page in data.asMap().entries) { + for (var item in page.value.items ?? []) { + if (item is AlbumSimple) { + albums.add(item); + } else if (item is PlaylistSimple) { + playlists.add(item); + } else if (item is Artist) { + artists.add(item); + } else if (item is Track) { + tracks.add(item); } } - return Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 20, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (tracks.isNotEmpty) - Text( - "Songs", - style: Theme.of(context).textTheme.headline5, + } + return Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 20, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (tracks.isNotEmpty) + Text( + "Songs", + style: Theme.of(context).textTheme.headline5, + ), + ...tracks.asMap().entries.map((track) { + String duration = + "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; + return TrackTile( + playback, + track: track, + duration: duration, + thumbnailUrl: + TypeConversionUtils.image_X_UrlString( + track.value.album?.images, + placeholder: ImagePlaceholder.albumArt, ), - ...tracks.asMap().entries.map((track) { - String duration = - "${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; - return TrackTile( - playback, - track: track, - duration: duration, - thumbnailUrl: - TypeConversionUtils.image_X_UrlString( - track.value.album?.images, - placeholder: ImagePlaceholder.albumArt, - ), - isActive: playback.track?.id == track.value.id, - onTrackPlayButtonPressed: (currentTrack) async { - var isPlaylistPlaying = - playback.playlist?.id != null && - playback.playlist?.id == - currentTrack.id; - if (!isPlaylistPlaying) { - playback.playPlaylist( - CurrentPlaylist( - tracks: [currentTrack], - id: currentTrack.id!, - name: currentTrack.name!, - thumbnail: TypeConversionUtils - .image_X_UrlString( - currentTrack.album?.images, - placeholder: - ImagePlaceholder.albumArt, - ), + isActive: playback.track?.id == track.value.id, + onTrackPlayButtonPressed: (currentTrack) async { + var isPlaylistPlaying = playback.playlist?.id != + null && + playback.playlist?.id == currentTrack.id; + if (!isPlaylistPlaying) { + playback.playPlaylist( + CurrentPlaylist( + tracks: [currentTrack], + id: currentTrack.id!, + name: currentTrack.name!, + thumbnail: + TypeConversionUtils.image_X_UrlString( + currentTrack.album?.images, + placeholder: ImagePlaceholder.albumArt, + ), + ), + ); + } else if (isPlaylistPlaying && + currentTrack.id != null && + currentTrack.id != playback.track?.id) { + playback.play(currentTrack); + } + }, + ); + }), + if (albums.isNotEmpty) + Text( + "Albums", + style: Theme.of(context).textTheme.headline5, + ), + const SizedBox(height: 10), + ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + controller: albumController, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: albumController, + child: Row( + children: albums.map((album) { + return AlbumCard( + TypeConversionUtils.simpleAlbum_X_Album( + album, ), ); - } else if (isPlaylistPlaying && - currentTrack.id != null && - currentTrack.id != playback.track?.id) { - playback.play(currentTrack); - } - }, - ); - }), - if (albums.isNotEmpty) - Text( - "Albums", - style: Theme.of(context).textTheme.headline5, - ), - const SizedBox(height: 10), - ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - controller: albumController, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: albumController, - child: Row( - children: albums.map((album) { - return AlbumCard( - TypeConversionUtils.simpleAlbum_X_Album( - album, - ), - ); - }).toList(), - ), + }).toList(), ), ), ), - const SizedBox(height: 20), - if (artists.isNotEmpty) - Text( - "Artists", - style: Theme.of(context).textTheme.headline5, - ), - const SizedBox(height: 10), - ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( + ), + const SizedBox(height: 20), + if (artists.isNotEmpty) + Text( + "Artists", + style: Theme.of(context).textTheme.headline5, + ), + const SizedBox(height: 10), + ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + controller: artistController, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, controller: artistController, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: artistController, - child: Row( - children: artists - .map( - (artist) => Container( - margin: const EdgeInsets.symmetric( - horizontal: 15), - child: ArtistCard(artist), - ), - ) - .toList(), - ), + child: Row( + children: artists + .map( + (artist) => Container( + margin: const EdgeInsets.symmetric( + horizontal: 15), + child: ArtistCard(artist), + ), + ) + .toList(), ), ), ), - const SizedBox(height: 20), - if (playlists.isNotEmpty) - Text( - "Playlists", - style: Theme.of(context).textTheme.headline5, - ), - const SizedBox(height: 10), - ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( - scrollbarOrientation: - breakpoint > Breakpoints.md - ? ScrollbarOrientation.bottom - : ScrollbarOrientation.top, + ), + const SizedBox(height: 20), + if (playlists.isNotEmpty) + Text( + "Playlists", + style: Theme.of(context).textTheme.headline5, + ), + const SizedBox(height: 10), + ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + scrollbarOrientation: breakpoint > Breakpoints.md + ? ScrollbarOrientation.bottom + : ScrollbarOrientation.top, + controller: playlistController, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, controller: playlistController, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: playlistController, - child: Row( - children: playlists - .map( - (playlist) => PlaylistCard(playlist), - ) - .toList(), - ), + child: Row( + children: playlists + .map( + (playlist) => PlaylistCard(playlist), + ) + .toList(), ), ), ), - ], - ), + ), + ], ), ), - ); - }, - error: (error, __) => Text("Error $error"), - loading: () => const CircularProgressIndicator(), - ) - ], - ), + ), + ); + }, + error: (error, __) => Text("Error $error"), + loading: () => const CircularProgressIndicator(), + ) + ], ), ), ); diff --git a/lib/components/Shared/AnonymousFallback.dart b/lib/components/Shared/AnonymousFallback.dart index d5fe727c..3c8e1931 100644 --- a/lib/components/Shared/AnonymousFallback.dart +++ b/lib/components/Shared/AnonymousFallback.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; import 'package:spotube/provider/Auth.dart'; +import 'package:spotube/utils/service_utils.dart'; class AnonymousFallback extends ConsumerWidget { final Widget? child; @@ -23,7 +23,7 @@ class AnonymousFallback extends ConsumerWidget { const SizedBox(height: 10), ElevatedButton( child: const Text("Login with Spotify"), - onPressed: () => GoRouter.of(context).push("/settings"), + onPressed: () => ServiceUtils.navigate(context, "/settings"), ) ], ), diff --git a/lib/components/Shared/LinkText.dart b/lib/components/Shared/LinkText.dart index d24aaf75..b5868def 100644 --- a/lib/components/Shared/LinkText.dart +++ b/lib/components/Shared/LinkText.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:spotube/components/Shared/AnchorButton.dart'; +import 'package:spotube/utils/service_utils.dart'; class LinkText extends StatelessWidget { final String text; @@ -24,7 +24,7 @@ class LinkText extends StatelessWidget { return AnchorButton( text, onTap: () { - GoRouter.of(context).push(route, extra: extra); + ServiceUtils.navigate(context, route, extra: extra); }, key: key, overflow: overflow, diff --git a/lib/components/Shared/TrackCollectionView.dart b/lib/components/Shared/TrackCollectionView.dart index edcb3cc0..2e7e16b2 100644 --- a/lib/components/Shared/TrackCollectionView.dart +++ b/lib/components/Shared/TrackCollectionView.dart @@ -1,4 +1,3 @@ -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'; diff --git a/lib/main.dart b/lib/main.dart index 994f12c8..ebc4312b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,6 @@ import 'package:fl_query/fl_query.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:hive_flutter/hive_flutter.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -26,7 +25,7 @@ import 'package:spotube/themes/dark-theme.dart'; import 'package:spotube/themes/light-theme.dart'; import 'package:spotube/utils/platform.dart'; -final bowl = QueryBowl(); +final bowl = QueryBowl(refetchOnExternalDataChange: true); void main() async { await Hive.initFlutter(); Hive.registerAdapter(CacheTrackAdapter()); @@ -144,7 +143,6 @@ class Spotube extends StatefulHookConsumerWidget { } class _SpotubeState extends ConsumerState with WidgetsBindingObserver { - final GoRouter _router = createGoRouter(); final logger = getLogger(Spotube); SharedPreferences? localStorage; @@ -201,8 +199,7 @@ class _SpotubeState extends ConsumerState with WidgetsBindingObserver { }, []); return MaterialApp.router( - routeInformationParser: _router.routeInformationParser, - routerDelegate: _router.routerDelegate, + routerConfig: router, debugShowCheckedModeBanner: false, title: 'Spotube', theme: lightTheme( @@ -218,7 +215,7 @@ class _SpotubeState extends ConsumerState with WidgetsBindingObserver { ...WidgetsApp.defaultShortcuts, const SingleActivator(LogicalKeyboardKey.space): PlayPauseIntent(ref), const SingleActivator(LogicalKeyboardKey.comma, control: true): - NavigationIntent(_router, "/settings"), + NavigationIntent(router, "/settings"), const SingleActivator( LogicalKeyboardKey.keyB, control: true, diff --git a/lib/models/GoRouteDeclarations.dart b/lib/models/GoRouteDeclarations.dart index 0db6a56d..b368a627 100644 --- a/lib/models/GoRouteDeclarations.dart +++ b/lib/models/GoRouteDeclarations.dart @@ -1,34 +1,51 @@ +import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Search; import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart'; -import 'package:spotube/components/Home/Home.dart'; +import 'package:spotube/components/Home/Genres.dart'; +import 'package:spotube/components/Home/Shell.dart'; +import 'package:spotube/components/Library/UserLibrary.dart'; import 'package:spotube/components/Login/LoginTutorial.dart'; import 'package:spotube/components/Login/TokenLogin.dart'; +import 'package:spotube/components/Lyrics/SyncedLyrics.dart'; import 'package:spotube/components/Player/PlayerView.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart'; +import 'package:spotube/components/Search/Search.dart'; import 'package:spotube/components/Settings/Settings.dart'; import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/components/Login/WebViewLogin.dart'; -GoRouter createGoRouter() => GoRouter( +final rootNavigatorKey = GlobalKey(); +final shellRouteNavigatorKey = GlobalKey(); +final router = GoRouter( + navigatorKey: rootNavigatorKey, + routes: [ + ShellRoute( + navigatorKey: shellRouteNavigatorKey, + builder: (context, state, child) => Shell(child: child), routes: [ GoRoute( path: "/", - builder: (context, state) => Home(), + pageBuilder: (context, state) => const SpotubePage(child: Genres()), ), GoRoute( - path: "/login", - pageBuilder: (context, state) => SpotubePage( - child: kIsMobile ? const WebViewLogin() : const TokenLogin(), - ), + path: "/search", + name: "Search", + pageBuilder: (context, state) => const SpotubePage(child: Search()), ), GoRoute( - path: "/login-tutorial", - pageBuilder: (context, state) => const SpotubePage( - child: LoginTutorial(), - ), + path: "/library", + name: "Library", + pageBuilder: (context, state) => + const SpotubePage(child: UserLibrary()), + ), + GoRoute( + path: "/lyrics", + name: "Lyrics", + pageBuilder: (context, state) => + const SpotubePage(child: SyncedLyrics()), ), GoRoute( path: "/settings", @@ -59,13 +76,30 @@ GoRouter createGoRouter() => GoRouter( ); }, ), - GoRoute( - path: "/player", - pageBuilder: (context, state) { - return const SpotubePage( - child: PlayerView(), - ); - }, - ), ], - ); + ), + GoRoute( + path: "/login", + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => SpotubePage( + child: kIsMobile ? const WebViewLogin() : const TokenLogin(), + ), + ), + GoRoute( + path: "/login-tutorial", + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) => const SpotubePage( + child: LoginTutorial(), + ), + ), + GoRoute( + path: "/player", + parentNavigatorKey: rootNavigatorKey, + pageBuilder: (context, state) { + return const SpotubePage( + child: PlayerView(), + ); + }, + ), + ], +); diff --git a/lib/provider/Auth.dart b/lib/provider/Auth.dart index 9586fd5f..c286b97c 100644 --- a/lib/provider/Auth.dart +++ b/lib/provider/Auth.dart @@ -30,7 +30,7 @@ class Auth extends PersistedChangeNotifier { Duration get expiresIn => _expiration?.difference(DateTime.now()) ?? Duration.zero; - _refresh() async { + refresh() async { final data = await ServiceUtils.getAccessToken(authCookie!); _accessToken = data.accessToken; _expiration = data.expiration; @@ -43,7 +43,7 @@ class Auth extends PersistedChangeNotifier { return null; } _refresher?.cancel(); - return Timer(expiresIn, _refresh); + return Timer(expiresIn, refresh); } void _restartRefresher() { @@ -103,7 +103,7 @@ class Auth extends PersistedChangeNotifier { _authCookie = map["authCookie"]; _restartRefresher(); if (isExpired) { - _refresh(); + refresh(); } } diff --git a/lib/provider/SpotifyRequests.dart b/lib/provider/SpotifyRequests.dart index f6a41342..c458966d 100644 --- a/lib/provider/SpotifyRequests.dart +++ b/lib/provider/SpotifyRequests.dart @@ -18,6 +18,7 @@ final categoriesQueryJob = initialParam: 0, getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset, getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 16, + refetchOnExternalDataChange: true, task: (queryKey, pageParam, data) async { final SpotifyApi spotify = data["spotify"] as SpotifyApi; final String recommendationMarket = data["recommendationMarket"]; diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 12fa5b20..376b7d30 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:io'; +import 'package:flutter/widgets.dart' hide Element; +import 'package:go_router/go_router.dart'; import 'package:html/dom.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotify/spotify.dart'; @@ -387,4 +389,8 @@ abstract class ServiceUtils { rethrow; } } + + static void navigate(BuildContext context, String location, {Object? extra}) { + GoRouter.of(context).push(location, extra: extra); + } }