feat: implemented go_route shell/nested route

BRIEF DESCRIPTION:
- Nested Routes like React-Router/Spotify Web/desktop
- Except Login routes everything is nested and wrapped by a Shell
- PlayerOverlay is no more a overlay
A really simple Sidebar now
This commit is contained in:
Kingkor Roy Tirtho 2022-10-10 20:00:47 +06:00
parent 25e6b236b8
commit 3e498a4827
24 changed files with 694 additions and 570 deletions

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/PlaybuttonCard.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/models/CurrentPlaylist.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class AlbumCard extends HookConsumerWidget { class AlbumCard extends HookConsumerWidget {
@ -33,7 +33,7 @@ class AlbumCard extends HookConsumerWidget {
description: description:
"Album • ${TypeConversionUtils.artists_X_String<ArtistSimple>(album.artists ?? [])}", "Album • ${TypeConversionUtils.artists_X_String<ArtistSimple>(album.artists ?? [])}",
onTap: () { onTap: () {
GoRouter.of(context).push("/album/${album.id}", extra: album); ServiceUtils.navigate(context, "/album/${album.id}", extra: album);
}, },
onPlaybuttonPressed: () async { onPlaybuttonPressed: () async {
SpotifyApi spotify = ref.read(spotifyProvider); SpotifyApi spotify = ref.read(spotifyProvider);

View File

@ -1,9 +1,9 @@
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/HoverBuilder.dart'; import 'package:spotube/components/Shared/HoverBuilder.dart';
import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class ArtistCard extends StatelessWidget { class ArtistCard extends StatelessWidget {
@ -23,7 +23,7 @@ class ArtistCard extends StatelessWidget {
width: 200, width: 200,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
GoRouter.of(context).push("/artist/${artist.id}"); ServiceUtils.navigate(context, "/artist/${artist.id}");
}, },
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: HoverBuilder(builder: (context, isHovering) { child: HoverBuilder(builder: (context, isHovering) {

View File

@ -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<Category?>(
(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);
},
),
);
}
}

View File

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

View File

@ -1,7 +1,6 @@
import 'package:badges/badges.dart'; import 'package:badges/badges.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/components/Shared/UniversalImage.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/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
final sidebarExtendedStateProvider = StateProvider<bool?>((ref) => null); final sidebarExtendedStateProvider = StateProvider<bool?>((ref) => null);
@ -35,7 +35,7 @@ class Sidebar extends HookConsumerWidget {
} }
static void goToSettings(BuildContext context) { static void goToSettings(BuildContext context) {
GoRouter.of(context).push("/settings"); ServiceUtils.navigate(context, "/settings");
} }
@override @override
@ -78,46 +78,52 @@ class Sidebar extends HookConsumerWidget {
!(forceExtended ?? extended.value); !(forceExtended ?? extended.value);
return SafeArea( return SafeArea(
top: false,
child: Material( child: Material(
color: Theme.of(context).navigationRailTheme.backgroundColor, color: Theme.of(context).navigationRailTheme.backgroundColor,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (selectedIndex == 3 && kIsDesktop) if (kIsDesktop)
SizedBox( SizedBox(
height: appWindow.titleBarHeight, height: appWindow.titleBarHeight,
width: extended.value ? 256 : 80, 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( if (!kIsDesktop && !extended.value)
padding: const EdgeInsets.only(left: 10), Center(
child: (extended.value) child: IconButton(
? Row( icon: const Icon(Icons.menu_rounded),
children: [ onPressed: toggleExtended,
_buildSmallLogo(), ),
const SizedBox( ),
width: 10, (extended.value)
), ? Row(
Text( children: [
"Spotube", _buildSmallLogo(),
style: Theme.of(context).textTheme.headline4, const SizedBox(
), width: 10,
IconButton( ),
icon: const Icon(Icons.menu_rounded), Text(
onPressed: toggleExtended, "Spotube",
), style: Theme.of(context).textTheme.headline4,
], ),
) IconButton(
: Column( icon: const Icon(Icons.menu_rounded),
children: [ onPressed: toggleExtended,
IconButton( ),
icon: const Icon(Icons.menu_rounded), ],
onPressed: toggleExtended, )
), : _buildSmallLogo(),
_buildSmallLogo(),
],
),
),
Expanded( Expanded(
child: NavigationRail( child: NavigationRail(
destinations: sidebarTileList.map( destinations: sidebarTileList.map(
@ -166,7 +172,7 @@ class Sidebar extends HookConsumerWidget {
); );
if (extended.value) { if (extended.value) {
return Padding( return Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16).copyWith(left: 0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View File

@ -12,32 +12,30 @@ class UserLibrary extends ConsumerWidget {
const UserLibrary({Key? key}) : super(key: key); const UserLibrary({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
return Expanded( return DefaultTabController(
child: DefaultTabController( length: 5,
length: 5, child: SafeArea(
child: SafeArea( child: Scaffold(
child: Scaffold( appBar: ColoredTabBar(
appBar: ColoredTabBar( color: Theme.of(context).backgroundColor,
color: Theme.of(context).backgroundColor, child: const TabBar(
child: const TabBar( isScrollable: true,
isScrollable: true, tabs: [
tabs: [ Tab(text: "Playlist"),
Tab(text: "Playlist"), Tab(text: "Downloads"),
Tab(text: "Downloads"), Tab(text: "Local"),
Tab(text: "Local"), Tab(text: "Artists"),
Tab(text: "Artists"), Tab(text: "Album"),
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()),
]),
), ),
), ),
); );

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:introduction_screen/introduction_screen.dart'; import 'package:introduction_screen/introduction_screen.dart';
import 'package:spotube/components/Login/TokenLoginForms.dart'; import 'package:spotube/components/Login/TokenLoginForms.dart';
import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/Hyperlink.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/utils/service_utils.dart';
class LoginTutorial extends ConsumerWidget { class LoginTutorial extends ConsumerWidget {
const LoginTutorial({Key? key}) : super(key: key); const LoginTutorial({Key? key}) : super(key: key);
@ -30,7 +30,7 @@ class LoginTutorial extends ConsumerWidget {
overrideDone: TextButton( overrideDone: TextButton(
onPressed: auth.isLoggedIn onPressed: auth.isLoggedIn
? () { ? () {
GoRouter.of(context).go("/"); ServiceUtils.navigate(context, "/");
} }
: null, : null,
child: const Text("Done"), child: const Text("Done"),

View File

@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/Login/TokenLoginForms.dart'; import 'package:spotube/components/Login/TokenLoginForms.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/utils/service_utils.dart';
class TokenLogin extends HookConsumerWidget { class TokenLogin extends HookConsumerWidget {
const TokenLogin({Key? key}) : super(key: key); const TokenLogin({Key? key}) : super(key: key);
@ -37,7 +38,7 @@ class TokenLogin extends HookConsumerWidget {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
TokenLoginForm( TokenLoginForm(
onDone: () => GoRouter.of(context).go("/"), onDone: () => ServiceUtils.navigate(context, "/"),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Wrap( Wrap(

View File

@ -61,7 +61,7 @@ class WebViewLogin extends HookConsumerWidget {
expiration: body.expiration, expiration: body.expiration,
); );
if (mounted()) { if (mounted()) {
GoRouter.of(context).go("/"); ServiceUtils.navigate(context, "/");
} }
} }
}, },

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyRequests.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
@ -25,7 +24,6 @@ class Lyrics extends HookConsumerWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
PageWindowTitleBar(foregroundColor: titleBarForegroundColor),
Center( Center(
child: Text( child: Text(
playback.track?.name ?? "", playback.track?.name ?? "",

View File

@ -1,7 +1,5 @@
import 'dart:ui'; 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/material.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';
@ -9,7 +7,6 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerLyrics.dart';
import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart'; import 'package:spotube/components/Lyrics/LyricDelayAdjustDialog.dart';
import 'package:spotube/components/Lyrics/Lyrics.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/SpotubeMarqueeText.dart';
import 'package:spotube/components/Shared/UniversalImage.dart'; import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useAutoScrollController.dart'; import 'package:spotube/hooks/useAutoScrollController.dart';
@ -129,130 +126,125 @@ class SyncedLyrics extends HookConsumerWidget {
noSetBGColor: true, noSetBGColor: true,
); );
return Expanded( return Container(
child: Container( clipBehavior: Clip.hardEdge,
clipBehavior: Clip.hardEdge, decoration: BoxDecoration(
decoration: BoxDecoration( image: DecorationImage(
image: DecorationImage( image: UniversalImage.imageProvider(albumArt),
image: UniversalImage.imageProvider(albumArt), fit: BoxFit.cover,
fit: BoxFit.cover,
),
), ),
child: BackdropFilter( ),
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), child: BackdropFilter(
child: Container( filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
color: palette.color.withOpacity(.7), child: Container(
child: SafeArea( color: palette.color.withOpacity(.7),
child: failed.value child: SafeArea(
? Lyrics(titleBarForegroundColor: palette.bodyTextColor) child: failed.value
: Column( ? Lyrics(titleBarForegroundColor: palette.bodyTextColor)
children: [ : Column(
PageWindowTitleBar( children: [
foregroundColor: palette.bodyTextColor, SizedBox(
), height: breakpoint >= Breakpoints.md ? 50 : 30,
SizedBox( child: Material(
height: breakpoint >= Breakpoints.md ? 50 : 30, type: MaterialType.transparency,
child: Material( child: Stack(
type: MaterialType.transparency, children: [
child: Stack( Center(
children: [ child: SpotubeMarqueeText(
Center( text: playback.track?.name ?? "Not Playing",
child: SpotubeMarqueeText( style: headlineTextStyle,
text: playback.track?.name ?? "Not Playing", isHovering: true,
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( Center(
TypeConversionUtils.artists_X_String<Artist>( child: Text(
playback.track?.artists ?? []), TypeConversionUtils.artists_X_String<Artist>(
style: breakpoint >= Breakpoints.md playback.track?.artists ?? []),
? textTheme.headline5 style: breakpoint >= Breakpoints.md
: textTheme.headline6, ? textTheme.headline5
), : textTheme.headline6,
), ),
if (lyricValue != null && lyricValue.lyrics.isNotEmpty) ),
Expanded( if (lyricValue != null && lyricValue.lyrics.isNotEmpty)
child: ListView.builder( Expanded(
controller: controller, child: ListView.builder(
itemCount: lyricValue.lyrics.length, controller: controller,
itemBuilder: (context, index) { itemCount: lyricValue.lyrics.length,
final lyricSlice = lyricValue.lyrics[index]; itemBuilder: (context, index) {
final isActive = final lyricSlice = lyricValue.lyrics[index];
lyricSlice.time.inSeconds == currentTime; final isActive =
lyricSlice.time.inSeconds == currentTime;
if (isActive) { if (isActive) {
controller.scrollToIndex( controller.scrollToIndex(
index, index,
preferPosition: AutoScrollPosition.middle, preferPosition: AutoScrollPosition.middle,
); );
} }
return AutoScrollTag( return AutoScrollTag(
key: ValueKey(index), key: ValueKey(index),
index: index, index: index,
controller: controller, controller: controller,
child: lyricSlice.text.isEmpty child: lyricSlice.text.isEmpty
? Container() ? Container()
: Center( : Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: AnimatedDefaultTextStyle( child: AnimatedDefaultTextStyle(
duration: const Duration( duration: const Duration(
milliseconds: 250), milliseconds: 250),
style: TextStyle( style: TextStyle(
color: isActive color: isActive
? Colors.white ? Colors.white
: palette.bodyTextColor, : palette.bodyTextColor,
fontWeight: isActive fontWeight: isActive
? FontWeight.bold ? FontWeight.bold
: FontWeight.normal, : FontWeight.normal,
fontSize: isActive ? 30 : 26, fontSize: isActive ? 30 : 26,
), ),
child: Text( child: Text(
lyricSlice.text, lyricSlice.text,
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
),
), ),
), ),
), ),
); ),
}, );
), },
), ),
if (playback.track != null && ),
(lyricValue == null || if (playback.track != null &&
lyricValue.lyrics.isEmpty == true)) (lyricValue == null ||
const Expanded(child: ShimmerLyrics()), lyricValue.lyrics.isEmpty == true))
], const Expanded(child: ShimmerLyrics()),
), ],
), ),
), ),
), ),
), ),

View File

@ -35,60 +35,15 @@ class Player extends HookConsumerWidget {
[playback.track?.album?.images], [playback.track?.album?.images],
); );
final entryRef = useRef<OverlayEntry?>(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 // returning an empty non spacious Container as the overlay will take
// place in the global overlay stack aka [_entries] // place in the global overlay stack aka [_entries]
if (layoutMode == LayoutMode.compact || if (layoutMode == LayoutMode.compact ||
(breakpoint.isLessThanOrEqualTo(Breakpoints.md) && (breakpoint.isLessThanOrEqualTo(Breakpoints.md) &&
layoutMode == LayoutMode.adaptive)) { 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( return Container(

View File

@ -5,11 +5,10 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/Player/PlayerTrackDetails.dart'; import 'package:spotube/components/Player/PlayerTrackDetails.dart';
import 'package:spotube/hooks/playback.dart'; import 'package:spotube/hooks/playback.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/hooks/usePaletteColor.dart';
import 'package:spotube/models/Intents.dart'; import 'package:spotube/models/Intents.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/utils/service_utils.dart';
class PlayerOverlay extends HookConsumerWidget { class PlayerOverlay extends HookConsumerWidget {
final String albumArt; final String albumArt;
@ -21,110 +20,84 @@ class PlayerOverlay extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final breakpoint = useBreakpoints();
final paletteColor = usePaletteColor(albumArt, ref); 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 onNext = useNextTrack(ref);
final onPrevious = usePreviousTrack(ref); final onPrevious = usePreviousTrack(ref);
if (!isHome && !isAllowedPage) return Container(); return GestureDetector(
onVerticalDragEnd: (details) {
return AnimatedPositioned( int sensitivity = 8;
duration: const Duration(milliseconds: 2500), if (details.primaryVelocity != null &&
right: (breakpoint.isMd && !isAllowedPage ? 10 : 5), details.primaryVelocity! < -sensitivity) {
left: (layoutMode == LayoutMode.compact || ServiceUtils.navigate(context, "/player");
(breakpoint.isSm && layoutMode == LayoutMode.adaptive) || }
isAllowedPage },
? 5 child: ClipRRect(
: 90), borderRadius: BorderRadius.circular(5),
bottom: (layoutMode == LayoutMode.compact && !isAllowedPage) || child: BackdropFilter(
(breakpoint.isSm && filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
layoutMode == LayoutMode.adaptive && child: AnimatedContainer(
!isAllowedPage) duration: const Duration(milliseconds: 500),
? 63 width: MediaQuery.of(context).size.width,
: 10, height: 50,
child: GestureDetector( decoration: BoxDecoration(
onVerticalDragEnd: (details) { color: paletteColor.color.withOpacity(.7),
int sensitivity = 8; border: Border.all(
if (details.primaryVelocity != null && color: paletteColor.titleTextColor,
details.primaryVelocity! < -sensitivity) { width: 2,
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),
), ),
child: Material( borderRadius: BorderRadius.circular(5),
type: MaterialType.transparency, ),
child: Row( child: Material(
mainAxisAlignment: MainAxisAlignment.spaceBetween, type: MaterialType.transparency,
children: [ child: Row(
Expanded( mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: MouseRegion( children: [
cursor: SystemMouseCursors.click, Expanded(
child: GestureDetector( child: MouseRegion(
onTap: () => GoRouter.of(context).push("/player"), cursor: SystemMouseCursors.click,
child: PlayerTrackDetails( child: GestureDetector(
albumArt: albumArt, onTap: () => GoRouter.of(context).push("/player"),
color: paletteColor.bodyTextColor, child: PlayerTrackDetails(
), albumArt: albumArt,
color: paletteColor.bodyTextColor,
), ),
), ),
), ),
Row( ),
children: [ Row(
IconButton( children: [
icon: const Icon(Icons.skip_previous_rounded), IconButton(
color: paletteColor.bodyTextColor, icon: const Icon(Icons.skip_previous_rounded),
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<PlayPauseIntent>(
context,
PlayPauseIntent(ref),
),
);
},
),
IconButton(
icon: const Icon(Icons.skip_next_rounded),
onPressed: () => onNext(),
color: paletteColor.bodyTextColor, 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<PlayPauseIntent>(
context,
PlayPauseIntent(ref),
),
);
},
),
IconButton(
icon: const Icon(Icons.skip_next_rounded),
onPressed: () => onNext(),
color: paletteColor.bodyTextColor,
),
],
),
],
), ),
), ),
), ),

View File

@ -7,6 +7,7 @@ import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/CurrentPlaylist.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
class PlaylistCard extends HookConsumerWidget { class PlaylistCard extends HookConsumerWidget {
@ -30,7 +31,8 @@ class PlaylistCard extends HookConsumerWidget {
isPlaying: isPlaylistPlaying && playback.isPlaying, isPlaying: isPlaylistPlaying && playback.isPlaying,
isLoading: playback.status == PlaybackStatus.loading && isPlaylistPlaying, isLoading: playback.status == PlaybackStatus.loading && isPlaylistPlaying,
onTap: () { onTap: () {
GoRouter.of(context).push( ServiceUtils.navigate(
context,
"/playlist/${playlist.id}", "/playlist/${playlist.id}",
extra: playlist, extra: playlist,
); );

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistGenreView extends ConsumerWidget { class PlaylistGenreView extends ConsumerWidget {

View File

@ -33,231 +33,223 @@ class Search extends HookConsumerWidget {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
if (auth.isAnonymous) { if (auth.isAnonymous) {
return const Expanded(child: AnonymousFallback()); return const AnonymousFallback();
} }
final searchSnapshot = ref.watch(searchQuery(searchTerm)); final searchSnapshot = ref.watch(searchQuery(searchTerm));
return Expanded( return SafeArea(
child: SafeArea( child: Material(
child: Material( color: Theme.of(context).backgroundColor,
color: Theme.of(context).backgroundColor, child: Column(
child: Column( children: [
children: [ Container(
Container( padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 20,
horizontal: 20, vertical: 10,
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;
},
),
), ),
searchSnapshot.when( color: Theme.of(context).backgroundColor,
data: (data) { child: TextField(
Playback playback = ref.watch(playbackProvider); controller: controller,
List<AlbumSimple> albums = []; decoration: InputDecoration(
List<Artist> artists = []; isDense: true,
List<Track> tracks = []; suffix: ElevatedButton(
List<PlaylistSimple> playlists = []; child: const Icon(Icons.search_rounded),
for (MapEntry<int, Page> page in data.asMap().entries) { onPressed: () {
for (var item in page.value.items ?? []) { ref.read(searchTermStateProvider.notifier).state =
if (item is AlbumSimple) { controller.value.text;
albums.add(item); },
} else if (item is PlaylistSimple) { ),
playlists.add(item); contentPadding: const EdgeInsets.symmetric(
} else if (item is Artist) { horizontal: 10,
artists.add(item); vertical: 7,
} else if (item is Track) { ),
tracks.add(item); 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<AlbumSimple> albums = [];
List<Artist> artists = [];
List<Track> tracks = [];
List<PlaylistSimple> playlists = [];
for (MapEntry<int, Page> 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( return Expanded(
child: Padding( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric( child: Padding(
vertical: 8, padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 8,
), horizontal: 20,
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
if (tracks.isNotEmpty) children: [
Text( if (tracks.isNotEmpty)
"Songs", Text(
style: Theme.of(context).textTheme.headline5, "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) { isActive: playback.track?.id == track.value.id,
String duration = onTrackPlayButtonPressed: (currentTrack) async {
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; var isPlaylistPlaying = playback.playlist?.id !=
return TrackTile( null &&
playback, playback.playlist?.id == currentTrack.id;
track: track, if (!isPlaylistPlaying) {
duration: duration, playback.playPlaylist(
thumbnailUrl: CurrentPlaylist(
TypeConversionUtils.image_X_UrlString( tracks: [currentTrack],
track.value.album?.images, id: currentTrack.id!,
placeholder: ImagePlaceholder.albumArt, name: currentTrack.name!,
), thumbnail:
isActive: playback.track?.id == track.value.id, TypeConversionUtils.image_X_UrlString(
onTrackPlayButtonPressed: (currentTrack) async { currentTrack.album?.images,
var isPlaylistPlaying = placeholder: ImagePlaceholder.albumArt,
playback.playlist?.id != null && ),
playback.playlist?.id == ),
currentTrack.id; );
if (!isPlaylistPlaying) { } else if (isPlaylistPlaying &&
playback.playPlaylist( currentTrack.id != null &&
CurrentPlaylist( currentTrack.id != playback.track?.id) {
tracks: [currentTrack], playback.play(currentTrack);
id: currentTrack.id!, }
name: currentTrack.name!, },
thumbnail: TypeConversionUtils );
.image_X_UrlString( }),
currentTrack.album?.images, if (albums.isNotEmpty)
placeholder: Text(
ImagePlaceholder.albumArt, "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 && }).toList(),
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(),
),
), ),
), ),
), ),
const SizedBox(height: 20), ),
if (artists.isNotEmpty) const SizedBox(height: 20),
Text( if (artists.isNotEmpty)
"Artists", Text(
style: Theme.of(context).textTheme.headline5, "Artists",
), style: Theme.of(context).textTheme.headline5,
const SizedBox(height: 10), ),
ScrollConfiguration( const SizedBox(height: 10),
behavior: ScrollConfiguration(
ScrollConfiguration.of(context).copyWith( behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: { dragDevices: {
PointerDeviceKind.touch, PointerDeviceKind.touch,
PointerDeviceKind.mouse, PointerDeviceKind.mouse,
}, },
), ),
child: Scrollbar( child: Scrollbar(
controller: artistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: artistController, controller: artistController,
child: SingleChildScrollView( child: Row(
scrollDirection: Axis.horizontal, children: artists
controller: artistController, .map(
child: Row( (artist) => Container(
children: artists margin: const EdgeInsets.symmetric(
.map( horizontal: 15),
(artist) => Container( child: ArtistCard(artist),
margin: const EdgeInsets.symmetric( ),
horizontal: 15), )
child: ArtistCard(artist), .toList(),
),
)
.toList(),
),
), ),
), ),
), ),
const SizedBox(height: 20), ),
if (playlists.isNotEmpty) const SizedBox(height: 20),
Text( if (playlists.isNotEmpty)
"Playlists", Text(
style: Theme.of(context).textTheme.headline5, "Playlists",
), style: Theme.of(context).textTheme.headline5,
const SizedBox(height: 10), ),
ScrollConfiguration( const SizedBox(height: 10),
behavior: ScrollConfiguration(
ScrollConfiguration.of(context).copyWith( behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: { dragDevices: {
PointerDeviceKind.touch, PointerDeviceKind.touch,
PointerDeviceKind.mouse, PointerDeviceKind.mouse,
}, },
), ),
child: Scrollbar( child: Scrollbar(
scrollbarOrientation: scrollbarOrientation: breakpoint > Breakpoints.md
breakpoint > Breakpoints.md ? ScrollbarOrientation.bottom
? ScrollbarOrientation.bottom : ScrollbarOrientation.top,
: ScrollbarOrientation.top, controller: playlistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: playlistController, controller: playlistController,
child: SingleChildScrollView( child: Row(
scrollDirection: Axis.horizontal, children: playlists
controller: playlistController, .map(
child: Row( (playlist) => PlaylistCard(playlist),
children: playlists )
.map( .toList(),
(playlist) => PlaylistCard(playlist),
)
.toList(),
),
), ),
), ),
), ),
], ),
), ],
), ),
), ),
); ),
}, );
error: (error, __) => Text("Error $error"), },
loading: () => const CircularProgressIndicator(), error: (error, __) => Text("Error $error"),
) loading: () => const CircularProgressIndicator(),
], )
), ],
), ),
), ),
); );

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/utils/service_utils.dart';
class AnonymousFallback extends ConsumerWidget { class AnonymousFallback extends ConsumerWidget {
final Widget? child; final Widget? child;
@ -23,7 +23,7 @@ class AnonymousFallback extends ConsumerWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
ElevatedButton( ElevatedButton(
child: const Text("Login with Spotify"), child: const Text("Login with Spotify"),
onPressed: () => GoRouter.of(context).push("/settings"), onPressed: () => ServiceUtils.navigate(context, "/settings"),
) )
], ],
), ),

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:spotube/components/Shared/AnchorButton.dart'; import 'package:spotube/components/Shared/AnchorButton.dart';
import 'package:spotube/utils/service_utils.dart';
class LinkText<T> extends StatelessWidget { class LinkText<T> extends StatelessWidget {
final String text; final String text;
@ -24,7 +24,7 @@ class LinkText<T> extends StatelessWidget {
return AnchorButton( return AnchorButton(
text, text,
onTap: () { onTap: () {
GoRouter.of(context).push(route, extra: extra); ServiceUtils.navigate(context, route, extra: extra);
}, },
key: key, key: key,
overflow: overflow, overflow: overflow,

View File

@ -1,4 +1,3 @@
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:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';

View File

@ -6,7 +6,6 @@ import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.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/themes/light-theme.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
final bowl = QueryBowl(); final bowl = QueryBowl(refetchOnExternalDataChange: true);
void main() async { void main() async {
await Hive.initFlutter(); await Hive.initFlutter();
Hive.registerAdapter(CacheTrackAdapter()); Hive.registerAdapter(CacheTrackAdapter());
@ -144,7 +143,6 @@ class Spotube extends StatefulHookConsumerWidget {
} }
class _SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver { class _SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
final GoRouter _router = createGoRouter();
final logger = getLogger(Spotube); final logger = getLogger(Spotube);
SharedPreferences? localStorage; SharedPreferences? localStorage;
@ -201,8 +199,7 @@ class _SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
}, []); }, []);
return MaterialApp.router( return MaterialApp.router(
routeInformationParser: _router.routeInformationParser, routerConfig: router,
routerDelegate: _router.routerDelegate,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'Spotube', title: 'Spotube',
theme: lightTheme( theme: lightTheme(
@ -218,7 +215,7 @@ class _SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
...WidgetsApp.defaultShortcuts, ...WidgetsApp.defaultShortcuts,
const SingleActivator(LogicalKeyboardKey.space): PlayPauseIntent(ref), const SingleActivator(LogicalKeyboardKey.space): PlayPauseIntent(ref),
const SingleActivator(LogicalKeyboardKey.comma, control: true): const SingleActivator(LogicalKeyboardKey.comma, control: true):
NavigationIntent(_router, "/settings"), NavigationIntent(router, "/settings"),
const SingleActivator( const SingleActivator(
LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyB,
control: true, control: true,

View File

@ -1,34 +1,51 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.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/Album/AlbumView.dart';
import 'package:spotube/components/Artist/ArtistProfile.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/LoginTutorial.dart';
import 'package:spotube/components/Login/TokenLogin.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/Player/PlayerView.dart';
import 'package:spotube/components/Playlist/PlaylistView.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/Settings/Settings.dart';
import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/components/Shared/SpotubePageRoute.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/components/Login/WebViewLogin.dart'; import 'package:spotube/components/Login/WebViewLogin.dart';
GoRouter createGoRouter() => GoRouter( final rootNavigatorKey = GlobalKey<NavigatorState>();
final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
final router = GoRouter(
navigatorKey: rootNavigatorKey,
routes: [
ShellRoute(
navigatorKey: shellRouteNavigatorKey,
builder: (context, state, child) => Shell(child: child),
routes: [ routes: [
GoRoute( GoRoute(
path: "/", path: "/",
builder: (context, state) => Home(), pageBuilder: (context, state) => const SpotubePage(child: Genres()),
), ),
GoRoute( GoRoute(
path: "/login", path: "/search",
pageBuilder: (context, state) => SpotubePage( name: "Search",
child: kIsMobile ? const WebViewLogin() : const TokenLogin(), pageBuilder: (context, state) => const SpotubePage(child: Search()),
),
), ),
GoRoute( GoRoute(
path: "/login-tutorial", path: "/library",
pageBuilder: (context, state) => const SpotubePage( name: "Library",
child: LoginTutorial(), pageBuilder: (context, state) =>
), const SpotubePage(child: UserLibrary()),
),
GoRoute(
path: "/lyrics",
name: "Lyrics",
pageBuilder: (context, state) =>
const SpotubePage(child: SyncedLyrics()),
), ),
GoRoute( GoRoute(
path: "/settings", 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(),
);
},
),
],
);

View File

@ -30,7 +30,7 @@ class Auth extends PersistedChangeNotifier {
Duration get expiresIn => Duration get expiresIn =>
_expiration?.difference(DateTime.now()) ?? Duration.zero; _expiration?.difference(DateTime.now()) ?? Duration.zero;
_refresh() async { refresh() async {
final data = await ServiceUtils.getAccessToken(authCookie!); final data = await ServiceUtils.getAccessToken(authCookie!);
_accessToken = data.accessToken; _accessToken = data.accessToken;
_expiration = data.expiration; _expiration = data.expiration;
@ -43,7 +43,7 @@ class Auth extends PersistedChangeNotifier {
return null; return null;
} }
_refresher?.cancel(); _refresher?.cancel();
return Timer(expiresIn, _refresh); return Timer(expiresIn, refresh);
} }
void _restartRefresher() { void _restartRefresher() {
@ -103,7 +103,7 @@ class Auth extends PersistedChangeNotifier {
_authCookie = map["authCookie"]; _authCookie = map["authCookie"];
_restartRefresher(); _restartRefresher();
if (isExpired) { if (isExpired) {
_refresh(); refresh();
} }
} }

View File

@ -18,6 +18,7 @@ final categoriesQueryJob =
initialParam: 0, initialParam: 0,
getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset, getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset,
getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 16, getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 16,
refetchOnExternalDataChange: true,
task: (queryKey, pageParam, data) async { task: (queryKey, pageParam, data) async {
final SpotifyApi spotify = data["spotify"] as SpotifyApi; final SpotifyApi spotify = data["spotify"] as SpotifyApi;
final String recommendationMarket = data["recommendationMarket"]; final String recommendationMarket = data["recommendationMarket"];

View File

@ -1,6 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/widgets.dart' hide Element;
import 'package:go_router/go_router.dart';
import 'package:html/dom.dart'; import 'package:html/dom.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';
@ -387,4 +389,8 @@ abstract class ServiceUtils {
rethrow; rethrow;
} }
} }
static void navigate(BuildContext context, String location, {Object? extra}) {
GoRouter.of(context).push(location, extra: extra);
}
} }