mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
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:
parent
25e6b236b8
commit
3e498a4827
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
64
lib/components/Home/Genres.dart
Normal file
64
lib/components/Home/Genres.dart
Normal 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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
106
lib/components/Home/Shell.dart
Normal file
106
lib/components/Home/Shell.dart
Normal 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]!);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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: [
|
||||||
|
@ -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()),
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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"),
|
||||||
|
@ -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(
|
||||||
|
@ -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, "/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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 ?? "",
|
||||||
|
@ -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()),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
@ -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 {
|
||||||
|
@ -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(),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
|
@ -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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"];
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user