From c77b0e198b215180d863747e35998a17aff92720 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 2 Oct 2022 11:04:27 +0600 Subject: [PATCH] feat: smoother list using fl_query and waypoint fix(theme): remove splash effect feat(artists-albums): horizontal paginated list instead of grid view page --- lib/components/Artist/ArtistAlbumList.dart | 73 ++++++++++++++ lib/components/Artist/ArtistAlbumView.dart | 95 ------------------- lib/components/Artist/ArtistProfile.dart | 48 ++-------- lib/components/Category/CategoryCard.dart | 78 +++++++-------- lib/components/Home/Home.dart | 71 +++++++------- lib/components/Library/UserArtists.dart | 57 ++++++----- lib/components/Library/UserLibrary.dart | 32 ++++--- lib/components/Search/Search.dart | 8 +- lib/components/Shared/ColoredTabBar.dart | 14 +++ lib/components/Shared/Waypoint.dart | 29 ++++++ lib/hooks/usePaginatedFutureProvider.dart | 42 -------- lib/main.dart | 7 +- lib/models/GoRouteDeclarations.dart | 14 --- lib/provider/SpotifyRequests.dart | 63 ++++++++---- lib/themes/dark-theme.dart | 3 +- lib/themes/light-theme.dart | 3 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 70 ++++++++++++++ pubspec.yaml | 3 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 21 files changed, 385 insertions(+), 331 deletions(-) create mode 100644 lib/components/Artist/ArtistAlbumList.dart delete mode 100644 lib/components/Artist/ArtistAlbumView.dart create mode 100644 lib/components/Shared/ColoredTabBar.dart create mode 100644 lib/components/Shared/Waypoint.dart delete mode 100644 lib/hooks/usePaginatedFutureProvider.dart diff --git a/lib/components/Artist/ArtistAlbumList.dart b/lib/components/Artist/ArtistAlbumList.dart new file mode 100644 index 000000000..418140564 --- /dev/null +++ b/lib/components/Artist/ArtistAlbumList.dart @@ -0,0 +1,73 @@ +import 'package:fl_query_hooks/fl_query_hooks.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart' hide Page; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Album/AlbumCard.dart'; +import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart'; +import 'package:spotube/components/Shared/Waypoint.dart'; +import 'package:spotube/models/Logger.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; + +class ArtistAlbumList extends HookConsumerWidget { + final String artistId; + ArtistAlbumList( + this.artistId, { + Key? key, + }) : super(key: key); + + final logger = getLogger(ArtistAlbumList); + + @override + Widget build(BuildContext context, ref) { + final scrollController = useScrollController(); + final albumsQuery = useInfiniteQuery( + job: artistAlbumsQueryJob(artistId), + externalData: ref.watch(spotifyProvider), + ); + + final albums = useMemoized(() { + return albumsQuery.pages + .expand((page) => page?.items ?? const Iterable.empty()) + .toList(); + }, [albumsQuery.pages]); + + final hasNextPage = albumsQuery.pages.isEmpty + ? false + : (albumsQuery.pages.last?.items?.length ?? 0) == 5; + + return SizedBox( + height: 300, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + interactive: false, + controller: scrollController, + child: ListView.builder( + itemCount: albums.length, + controller: scrollController, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + if (index == albums.length - 1 && hasNextPage) { + return Waypoint( + onEnter: () { + albumsQuery.fetchNextPage(); + }, + child: const ShimmerPlaybuttonCard(count: 1), + ); + } + return AlbumCard(albums[index]); + }, + ), + ), + ), + ); + } +} diff --git a/lib/components/Artist/ArtistAlbumView.dart b/lib/components/Artist/ArtistAlbumView.dart deleted file mode 100644 index 488575bb1..000000000 --- a/lib/components/Artist/ArtistAlbumView.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flutter/material.dart' hide Page; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Album/AlbumCard.dart'; -import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; -import 'package:spotube/models/Logger.dart'; -import 'package:spotube/provider/SpotifyDI.dart'; - -class ArtistAlbumView extends ConsumerStatefulWidget { - final String artistId; - final String artistName; - const ArtistAlbumView( - this.artistId, - this.artistName, { - Key? key, - }) : super(key: key); - - @override - ConsumerState createState() => _ArtistAlbumViewState(); -} - -class _ArtistAlbumViewState extends ConsumerState { - final PagingController _pagingController = - PagingController(firstPageKey: 0); - - final logger = getLogger(ArtistAlbumView); - - @override - void initState() { - super.initState(); - _pagingController.addPageRequestListener((pageKey) { - _fetchPage(pageKey); - }); - } - - @override - void dispose() { - _pagingController.dispose(); - super.dispose(); - } - - _fetchPage(int pageKey) async { - try { - SpotifyApi spotifyApi = ref.watch(spotifyProvider); - Page albums = - await spotifyApi.artists.albums(widget.artistId).getPage(8, pageKey); - - var items = albums.items!.toList(); - - if (albums.isLast && albums.items != null) { - _pagingController.appendLastPage(items); - } else if (albums.items != null) { - _pagingController.appendPage(items, albums.nextOffset); - } - } catch (e, stack) { - logger.e(e, null, stack); - _pagingController.error = e; - } - } - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: const PageWindowTitleBar(leading: BackButton()), - body: Column( - children: [ - Text( - widget.artistName, - style: Theme.of(context).textTheme.headline4, - ), - Expanded( - child: PagedGridView( - pagingController: _pagingController, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 260, - childAspectRatio: 9 / 13, - crossAxisSpacing: 20, - mainAxisSpacing: 20, - ), - padding: const EdgeInsets.all(10), - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - return AlbumCard(item); - }, - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index 7ace926d1..6a755c095 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Album/AlbumCard.dart'; +import 'package:spotube/components/Artist/ArtistAlbumList.dart'; import 'package:spotube/components/Artist/ArtistCard.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerArtistProfile.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; @@ -28,7 +27,6 @@ class ArtistProfile extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { SpotifyApi spotify = ref.watch(spotifyProvider); - final scrollController = useScrollController(); final parentScrollController = useScrollController(); final textTheme = Theme.of(context).textTheme; final chipTextVariant = useBreakpointValue( @@ -55,7 +53,7 @@ class ArtistProfile extends HookConsumerWidget { final isFollowingSnapshot = ref.watch(currentUserFollowsArtistQuery(artistId)); final topTracksSnapshot = ref.watch(artistTopTracksQuery(artistId)); - final albums = ref.watch(artistAlbumsQuery(artistId)); + final relatedArtists = ref.watch(artistRelatedArtistsQuery(artistId)); return SafeArea( @@ -263,46 +261,12 @@ class ArtistProfile extends HookConsumerWidget { child: CircularProgressIndicator.adaptive()), ), const SizedBox(height: 50), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Albums", - style: Theme.of(context).textTheme.headline4, - ), - TextButton( - child: const Text("See All"), - onPressed: () { - GoRouter.of(context).push( - "/artist-album/$artistId", - extra: data.name ?? "KRTX", - ); - }, - ) - ], + Text( + "Albums", + style: Theme.of(context).textTheme.headline4, ), const SizedBox(height: 10), - albums.when( - data: (albums) { - return Scrollbar( - controller: scrollController, - child: SingleChildScrollView( - controller: scrollController, - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: albums.items - ?.map((album) => AlbumCard(album)) - .toList() ?? - [], - ), - ), - ); - }, - error: (error, stackTrack) => - Text("Failed to get Artist albums $error"), - loading: () => const CircularProgressIndicator.adaptive(), - ), + ArtistAlbumList(artistId), const SizedBox(height: 20), Text( "Fans also likes", diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index c5eb9ccfb..550255265 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.dart @@ -1,14 +1,14 @@ +import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; -import 'package:spotube/components/Shared/NotFound.dart'; -import 'package:spotube/hooks/usePaginatedFutureProvider.dart'; +import 'package:spotube/components/Shared/Waypoint.dart'; import 'package:spotube/models/Logger.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; class CategoryCard extends HookConsumerWidget { @@ -25,29 +25,20 @@ class CategoryCard extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final scrollController = useScrollController(); - final mounted = useIsMounted(); - - final pagingController = - usePaginatedFutureProvider, int, PlaylistSimple>( - (pageKey) => categoryPlaylistsQuery( - [ - category.id, - pageKey, - ].join("/"), - ), - ref: ref, - firstPageKey: 0, - onData: (page, pagingController, pageKey) { - if (playlists != null && playlists?.isNotEmpty == true && mounted()) { - return pagingController.appendLastPage(playlists!.toList()); - } - if (page.isLast && page.items != null) { - pagingController.appendLastPage(page.items!.toList()); - } else if (page.items != null) { - pagingController.appendPage(page.items!.toList(), page.nextOffset); - } - }, + final spotify = ref.watch(spotifyProvider); + final playlistQuery = useInfiniteQuery( + job: categoryPlaylistsQueryJob(category.id!), + externalData: spotify, ); + final hasNextPage = playlistQuery.pages.isEmpty + ? false + : (playlistQuery.pages.last?.items?.length ?? 0) == 5; + + final playlists = playlistQuery.pages + .expand( + (page) => page?.items ?? const Iterable.empty(), + ) + .toList(); return Column( children: [ @@ -62,8 +53,8 @@ class CategoryCard extends HookConsumerWidget { ], ), ), - pagingController.error != null - ? const Text("Something Went Wrong") + playlistQuery.hasError + ? Text("Something Went Wrong\n${playlistQuery.errors.first}") : SizedBox( height: 245, child: ScrollConfiguration( @@ -76,26 +67,21 @@ class CategoryCard extends HookConsumerWidget { child: Scrollbar( controller: scrollController, interactive: false, - child: PagedListView( - shrinkWrap: true, - pagingController: pagingController, - scrollController: scrollController, + child: ListView.builder( scrollDirection: Axis.horizontal, - builderDelegate: - PagedChildBuilderDelegate( - noItemsFoundIndicatorBuilder: (context) { - return const NotFound(); - }, - firstPageProgressIndicatorBuilder: (context) { - return const ShimmerPlaybuttonCard(); - }, - newPageProgressIndicatorBuilder: (context) { - return const ShimmerPlaybuttonCard(); - }, - itemBuilder: (context, playlist, index) { - return PlaylistCard(playlist); - }, - ), + shrinkWrap: true, + itemCount: playlists.length, + itemBuilder: (context, index) { + if (index == playlists.length - 1 && hasNextPage) { + return Waypoint( + onEnter: () { + playlistQuery.fetchNextPage(); + }, + child: const ShimmerPlaybuttonCard(count: 1), + ); + } + return PlaylistCard(playlists[index]); + }, ), ), ), diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index e479384fa..f26a66e93 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -1,9 +1,9 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; +import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart' hide Page; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:spotify/spotify.dart' hide Image, Player, Search; import 'package:spotube/components/Category/CategoryCard.dart'; @@ -16,12 +16,14 @@ import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Player/Player.dart'; import 'package:spotube/components/Library/UserLibrary.dart'; +import 'package:spotube/components/Shared/Waypoint.dart'; import 'package:spotube/hooks/useBreakpointValue.dart'; -import 'package:spotube/hooks/usePaginatedFutureProvider.dart'; import 'package:spotube/hooks/useUpdateChecker.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Downloader.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; +import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/utils/platform.dart'; List spotifyScopes = [ @@ -144,38 +146,43 @@ class Home extends HookConsumerWidget { left: 8.0, ), child: HookBuilder(builder: (context) { - final pagingController = usePaginatedFutureProvider< - Page, int, Category>( - (pageKey) => categoriesQuery(pageKey), - ref: ref, - firstPageKey: 0, - onData: (categories, pagingController, pageKey) { - final items = categories.items?.toList(); - if (pageKey == 0) { - Category category = Category(); - category.id = "user-featured-playlists"; - category.name = "Featured"; - items?.insert(0, category); - } - if (categories.isLast && items != null) { - pagingController.appendLastPage(items); - } else if (categories.items != null) { - pagingController.appendPage( - items!, categories.nextOffset); - } + final spotify = ref.watch(spotifyProvider); + final recommendationMarket = ref.watch( + userPreferencesProvider + .select((s) => s.recommendationMarket), + ); + + final categoriesQuery = useInfiniteQuery( + job: categoriesQueryJob, + externalData: { + "spotify": spotify, + "recommendationMarket": recommendationMarket, }, ); - return PagedListView( - pagingController: pagingController, - builderDelegate: PagedChildBuilderDelegate( - firstPageProgressIndicatorBuilder: (_) => - const ShimmerCategories(), - newPageProgressIndicatorBuilder: (_) => - const ShimmerCategories(), - itemBuilder: (context, item, index) { - return CategoryCard(item); - }, - ), + + final categories = categoriesQuery.pages + .expand( + (page) => page?.items ?? const Iterable.empty(), + ) + .toList(); + + return ListView.builder( + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + if (category == null) return Container(); + if (index == categories.length - 1) { + return Waypoint( + onEnter: () { + if (categoriesQuery.hasNextPage) { + categoriesQuery.fetchNextPage(); + } + }, + child: const ShimmerCategories(), + ); + } + return CategoryCard(category); + }, ); }), ), diff --git a/lib/components/Library/UserArtists.dart b/lib/components/Library/UserArtists.dart index 4014ac1d8..1641bcb7a 100644 --- a/lib/components/Library/UserArtists.dart +++ b/lib/components/Library/UserArtists.dart @@ -1,35 +1,35 @@ +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:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Artist/ArtistCard.dart'; -import 'package:spotube/hooks/usePaginatedFutureProvider.dart'; -import 'package:spotube/models/Logger.dart'; +import 'package:spotube/components/Shared/Waypoint.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; class UserArtists extends HookConsumerWidget { - UserArtists({Key? key}) : super(key: key); - final logger = getLogger(UserArtists); + const UserArtists({Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { - final pagingController = - usePaginatedFutureProvider, String, Artist>( - (pageKey) => currentUserFollowingArtistsQuery(pageKey), - ref: ref, - firstPageKey: "", - onData: (artists, pagingController, pageKey) { - final items = artists.items!.toList(); - - if (artists.items != null && items.length < 15) { - pagingController.appendLastPage(items); - } else if (artists.items != null) { - pagingController.appendPage(items, items.last.id); - } - }, + final artistQuery = useInfiniteQuery( + job: currentUserFollowingArtistsQueryJob, + externalData: ref.watch(spotifyProvider), ); - return PagedGridView( + final artists = useMemoized( + () => artistQuery.pages + .expand((page) => page?.items ?? const Iterable.empty()) + .toList(), + [artistQuery.pages]); + + final hasNextPage = artistQuery.pages.isEmpty + ? false + : (artistQuery.pages.last?.items?.length ?? 0) == 15; + + return GridView.builder( + itemCount: artists.length, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 200, mainAxisExtent: 250, @@ -37,12 +37,17 @@ class UserArtists extends HookConsumerWidget { mainAxisSpacing: 20, ), padding: const EdgeInsets.all(10), - pagingController: pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - return ArtistCard(item); - }, - ), + itemBuilder: (context, index) { + if (index == artists.length - 1 && hasNextPage) { + return Waypoint( + onEnter: () { + artistQuery.fetchNextPage(); + }, + child: ArtistCard(artists[index]), + ); + } + return ArtistCard(artists[index]); + }, ); } } diff --git a/lib/components/Library/UserLibrary.dart b/lib/components/Library/UserLibrary.dart index 1dd350cc0..9d5047930 100644 --- a/lib/components/Library/UserLibrary.dart +++ b/lib/components/Library/UserLibrary.dart @@ -6,6 +6,7 @@ import 'package:spotube/components/Library/UserDownloads.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/components/Library/UserPlaylists.dart'; import 'package:spotube/components/Shared/AnonymousFallback.dart'; +import 'package:spotube/components/Shared/ColoredTabBar.dart'; class UserLibrary extends ConsumerWidget { const UserLibrary({Key? key}) : super(key: key); @@ -16,22 +17,25 @@ class UserLibrary extends ConsumerWidget { length: 5, child: SafeArea( child: Scaffold( - appBar: const TabBar( - isScrollable: true, - tabs: [ - Tab(text: "Playlist"), - Tab(text: "Downloads"), - Tab(text: "Local"), - Tab(text: "Artists"), - Tab(text: "Album"), - ], + appBar: ColoredTabBar( + color: Theme.of(context).backgroundColor, + child: const TabBar( + isScrollable: true, + tabs: [ + Tab(text: "Playlist"), + Tab(text: "Downloads"), + Tab(text: "Local"), + Tab(text: "Artists"), + Tab(text: "Album"), + ], + ), ), - body: TabBarView(children: [ - const AnonymousFallback(child: UserPlaylists()), - const UserDownloads(), - const UserLocalTracks(), + body: const TabBarView(children: [ + AnonymousFallback(child: UserPlaylists()), + UserDownloads(), + UserLocalTracks(), AnonymousFallback(child: UserArtists()), - const AnonymousFallback(child: UserAlbums()), + AnonymousFallback(child: UserAlbums()), ]), ), ), diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index b8762c4fd..1e019c21a 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -60,6 +60,10 @@ class Search extends HookConsumerWidget { controller.value.text; }, ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 7, + ), hintStyle: const TextStyle(height: 2), hintText: "Search...", ), @@ -93,7 +97,9 @@ class Search extends HookConsumerWidget { child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric( - vertical: 8, horizontal: 20), + vertical: 8, + horizontal: 20, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/components/Shared/ColoredTabBar.dart b/lib/components/Shared/ColoredTabBar.dart new file mode 100644 index 000000000..f528a7c5c --- /dev/null +++ b/lib/components/Shared/ColoredTabBar.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class ColoredTabBar extends ColoredBox implements PreferredSizeWidget { + final TabBar child; + + const ColoredTabBar({ + required super.color, + required this.child, + super.key, + }) : super(child: child); + + @override + Size get preferredSize => child.preferredSize; +} diff --git a/lib/components/Shared/Waypoint.dart b/lib/components/Shared/Waypoint.dart new file mode 100644 index 000000000..68e128e3f --- /dev/null +++ b/lib/components/Shared/Waypoint.dart @@ -0,0 +1,29 @@ +import 'package:flutter/cupertino.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +class Waypoint extends StatelessWidget { + final void Function()? onEnter; + final void Function()? onLeave; + final Widget? child; + const Waypoint({ + Key? key, + this.onEnter, + this.onLeave, + this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return VisibilityDetector( + key: const Key("waypoint"), + onVisibilityChanged: (info) { + if (info.visibleFraction == 0) { + onLeave?.call(); + } else if (info.visibleFraction > 0) { + onEnter?.call(); + } + }, + child: child ?? Container(), + ); + } +} diff --git a/lib/hooks/usePaginatedFutureProvider.dart b/lib/hooks/usePaginatedFutureProvider.dart deleted file mode 100644 index 94a925757..000000000 --- a/lib/hooks/usePaginatedFutureProvider.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:hookified_infinite_scroll_pagination/hookified_infinite_scroll_pagination.dart'; - -PagingController usePaginatedFutureProvider( - FutureProvider Function(P pageKey) createSnapshot, { - required P firstPageKey, - required WidgetRef ref, - void Function( - T, - PagingController pagingController, - P pageKey, - )? - onData, - void Function(Object)? onError, - void Function()? onLoading, -}) { - final currentPageKey = useState(firstPageKey); - final snapshot = ref.watch(createSnapshot(currentPageKey.value)); - final pagingController = usePagingController( - firstPageKey: firstPageKey, - onPageRequest: (pageKey, pagingController) { - if (currentPageKey.value != pageKey) { - currentPageKey.value = pageKey; - } - }); - - useEffect(() { - snapshot.whenOrNull( - data: (data) => - onData?.call(data, pagingController, currentPageKey.value), - error: (error, _) { - pagingController.error = error; - return onError?.call(error); - }, - loading: onLoading, - ); - return null; - }, [currentPageKey, snapshot]); - - return pagingController; -} diff --git a/lib/main.dart b/lib/main.dart index 65ad818bd..e55c2b7b8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:audio_service/audio_service.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; +import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -25,6 +26,7 @@ import 'package:spotube/themes/dark-theme.dart'; import 'package:spotube/themes/light-theme.dart'; import 'package:spotube/utils/platform.dart'; +final bowl = QueryBowl(); void main() async { await Hive.initFlutter(); Hive.registerAdapter(CacheTrackAdapter()); @@ -124,7 +126,10 @@ void main() async { ), ) ], - child: const Spotube(), + child: QueryBowlScope( + bowl: bowl, + child: const Spotube(), + ), ); }, ), diff --git a/lib/models/GoRouteDeclarations.dart b/lib/models/GoRouteDeclarations.dart index cb28a97fa..2f6b1b959 100644 --- a/lib/models/GoRouteDeclarations.dart +++ b/lib/models/GoRouteDeclarations.dart @@ -1,7 +1,6 @@ import 'package:go_router/go_router.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumView.dart'; -import 'package:spotube/components/Artist/ArtistAlbumView.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/components/Login/Login.dart'; @@ -49,19 +48,6 @@ GoRouter createGoRouter() => GoRouter( return SpotubePage(child: ArtistProfile(state.params["id"]!)); }, ), - GoRoute( - path: "/artist-album/:id", - pageBuilder: (context, state) { - assert(state.params["id"] != null); - assert(state.extra is String); - return SpotubePage( - child: ArtistAlbumView( - state.params["id"]!, - state.extra as String, - ), - ); - }, - ), GoRoute( path: "/playlist/:id", pageBuilder: (context, state) { diff --git a/lib/provider/SpotifyRequests.dart b/lib/provider/SpotifyRequests.dart index 5fbf527c6..f6a413427 100644 --- a/lib/provider/SpotifyRequests.dart +++ b/lib/provider/SpotifyRequests.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:fl_query/fl_query.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/LyricsModels.dart'; @@ -11,29 +12,35 @@ import 'package:collection/collection.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; -final categoriesQuery = FutureProvider.family, int>( - (ref, pageKey) { - final spotify = ref.watch(spotifyProvider); - final recommendationMarket = ref.watch( - userPreferencesProvider.select((s) => s.recommendationMarket), - ); - return spotify.categories +final categoriesQueryJob = + InfiniteQueryJob, Map, int>( + queryKey: "categories-query", + initialParam: 0, + getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset, + getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 16, + task: (queryKey, pageParam, data) async { + final SpotifyApi spotify = data["spotify"] as SpotifyApi; + final String recommendationMarket = data["recommendationMarket"]; + final categories = await spotify.categories .list(country: recommendationMarket) - .getPage(15, pageKey); + .getPage(15, pageParam); + + return categories; }, ); -final categoryPlaylistsQuery = - FutureProvider.family, String>( - (ref, value) { - final spotify = ref.watch(spotifyProvider); - final List data = value.split("/"); - final id = data.first; - final pageKey = data.last; +final categoryPlaylistsQueryJob = + InfiniteQueryJob.withVariableKey, SpotifyApi, int>( + preQueryKey: "category-playlists", + initialParam: 0, + getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset, + getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 6, + task: (queryKey, pageKey, spotify) { + final id = getVariable(queryKey); return (id != "user-featured-playlists" ? spotify.playlists.getByCategoryId(id) : spotify.playlists.featured) - .getPage(3, int.parse(pageKey)); + .getPage(5, pageKey); }, ); @@ -59,6 +66,18 @@ final currentUserFollowingArtistsQuery = }, ); +final currentUserFollowingArtistsQueryJob = + InfiniteQueryJob, SpotifyApi, String>( + queryKey: "user-following-artists", + initialParam: "", + getNextPageParam: (lastPage, lastParam) => lastPage.after, + getPreviousPageParam: (lastPage, lastParam) => + lastPage.metadata.previous ?? "", + task: (queryKey, pageKey, spotify) { + return spotify.me.following(FollowingType.artist).getPage(15, pageKey); + }, +); + final artistProfileQuery = FutureProvider.family( (ref, id) { final spotify = ref.watch(spotifyProvider); @@ -90,6 +109,18 @@ final artistAlbumsQuery = FutureProvider.family, String>( }, ); +final artistAlbumsQueryJob = + InfiniteQueryJob.withVariableKey, SpotifyApi, int>( + preQueryKey: "artist-albums", + initialParam: 0, + getNextPageParam: (lastPage, lastParam) => lastPage.nextOffset, + getPreviousPageParam: (lastPage, lastParam) => lastPage.nextOffset - 6, + task: (queryKey, pageKey, spotify) { + final id = getVariable(queryKey); + return spotify.artists.albums(id).getPage(5, pageKey); + }, +); + final artistRelatedArtistsQuery = FutureProvider.family, String>( (ref, id) { diff --git a/lib/themes/dark-theme.dart b/lib/themes/dark-theme.dart index 79c1f1000..057b380d7 100644 --- a/lib/themes/dark-theme.dart +++ b/lib/themes/dark-theme.dart @@ -15,6 +15,7 @@ ThemeData darkTheme({ ) ], primaryColor: accentMaterialColor, + splashFactory: NoSplash.splashFactory, primarySwatch: accentMaterialColor, backgroundColor: backgroundMaterialColor[900], scaffoldBackgroundColor: backgroundMaterialColor[900], @@ -56,7 +57,7 @@ ThemeData darkTheme({ ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - onPrimary: accentMaterialColor[300], + foregroundColor: accentMaterialColor[300], textStyle: const TextStyle( fontWeight: FontWeight.bold, ), diff --git a/lib/themes/light-theme.dart b/lib/themes/light-theme.dart index 4b8481002..114a99ca0 100644 --- a/lib/themes/light-theme.dart +++ b/lib/themes/light-theme.dart @@ -80,9 +80,10 @@ ThemeData lightTheme({ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), color: backgroundMaterialColor[50], ), + splashFactory: NoSplash.splashFactory, elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - onPrimary: accentMaterialColor[800], + foregroundColor: accentMaterialColor[800], textStyle: const TextStyle( fontWeight: FontWeight.bold, ), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3e375fb36..bc878ab6e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import audio_service import audio_session import audioplayers_darwin import bitsdojo_window_macos +import connectivity_plus_macos import package_info_plus_macos import path_provider_macos import shared_preferences_macos @@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) + ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index a7934b981..20f9fbd1a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -346,6 +346,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + connectivity_plus: + dependency: transitive + description: + name: connectivity_plus + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.7" + connectivity_plus_linux: + dependency: transitive + description: + name: connectivity_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + connectivity_plus_macos: + dependency: transitive + description: + name: connectivity_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.4" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + connectivity_plus_web: + dependency: transitive + description: + name: connectivity_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.4" + connectivity_plus_windows: + dependency: transitive + description: + name: connectivity_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" convert: dependency: transitive description: @@ -437,6 +479,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + fl_query: + dependency: "direct main" + description: + name: fl_query + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + fl_query_hooks: + dependency: "direct main" + description: + name: fl_query_hooks + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" flutter: dependency: "direct main" description: flutter @@ -732,6 +788,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + nm: + dependency: transitive + description: + name: nm + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.0" oauth2: dependency: transitive description: @@ -1262,6 +1325,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.2" + visibility_detector: + dependency: "direct main" + description: + name: visibility_detector + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1de47014f..b6a99b475 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,9 @@ dependencies: badges: ^2.0.3 mime: ^1.0.2 metadata_god: ^0.2.0 + visibility_detector: ^0.3.3 + fl_query: ^0.3.0 + fl_query_hooks: ^0.3.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index cf86bfe41..c40e4841a 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); BitsdojoWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); MetadataGodPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MetadataGodPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 767473459..5dca4667d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows bitsdojo_window_windows + connectivity_plus_windows metadata_god permission_handler_windows url_launcher_windows