From 69995bea1c6342c9212e5b22ef50bdfd6e7eba45 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 7 Dec 2022 10:27:46 +0600 Subject: [PATCH] fix: horizontal infinite lists doesn't fill the screen --- lib/components/Artist/ArtistAlbumList.dart | 27 ++-- lib/components/Category/CategoryCard.dart | 29 ++-- lib/components/Home/Genres.dart | 33 +++-- lib/components/Library/UserArtists.dart | 22 +-- lib/components/Search/Search.dart | 149 ++++++++++---------- lib/components/Shared/SpotubePageRoute.dart | 37 +++-- lib/components/Shared/Waypoint.dart | 58 ++++++-- lib/models/GoRouteDeclarations.dart | 20 +-- pubspec.lock | 8 +- pubspec.yaml | 4 +- 10 files changed, 212 insertions(+), 175 deletions(-) diff --git a/lib/components/Artist/ArtistAlbumList.dart b/lib/components/Artist/ArtistAlbumList.dart index 418140564..4791d5714 100644 --- a/lib/components/Artist/ArtistAlbumList.dart +++ b/lib/components/Artist/ArtistAlbumList.dart @@ -50,21 +50,22 @@ class ArtistAlbumList extends HookConsumerWidget { child: Scrollbar( interactive: false, controller: scrollController, - child: ListView.builder( - itemCount: albums.length, + child: Waypoint( 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]); + onTouchEdge: () { + albumsQuery.fetchNextPage(); }, + child: ListView.builder( + itemCount: albums.length, + controller: scrollController, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + if (index == albums.length - 1 && hasNextPage) { + return const ShimmerPlaybuttonCard(count: 1); + } + return AlbumCard(albums[index]); + }, + ), ), ), ), diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index c94b65f8f..bddc2169c 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.dart @@ -66,21 +66,22 @@ class CategoryCard extends HookConsumerWidget { child: Scrollbar( controller: scrollController, interactive: false, - child: ListView.builder( - scrollDirection: Axis.horizontal, - 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]); + child: Waypoint( + controller: scrollController, + onTouchEdge: () { + playlistQuery.fetchNextPage(); }, + child: ListView( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + controller: scrollController, + children: [ + ...playlists + .map((playlist) => PlaylistCard(playlist)), + if (hasNextPage) + const ShimmerPlaybuttonCard(count: 1), + ], + ), ), ), ), diff --git a/lib/components/Home/Genres.dart b/lib/components/Home/Genres.dart index e555aa764..b7af2ae20 100644 --- a/lib/components/Home/Genres.dart +++ b/lib/components/Home/Genres.dart @@ -18,6 +18,7 @@ class Genres extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final scrollController = useScrollController(); final spotify = ref.watch(spotifyProvider); final recommendationMarket = ref.watch( userPreferencesProvider.select((s) => s.recommendationMarket), @@ -45,23 +46,25 @@ class Genres extends HookConsumerWidget { return PlatformScaffold( appBar: kIsDesktop ? PageWindowTitleBar() : null, - 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(), - ); + body: Waypoint( + onTouchEdge: () { + if (categoriesQuery.hasNextPage) { + categoriesQuery.fetchNextPage(); } - return CategoryCard(category); }, + controller: scrollController, + child: ListView.builder( + controller: scrollController, + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + if (category == null) return Container(); + if (index == categories.length - 1) { + return const ShimmerCategories(); + } + return CategoryCard(category); + }, + ), ), ); } diff --git a/lib/components/Library/UserArtists.dart b/lib/components/Library/UserArtists.dart index 757193a13..bbe019f37 100644 --- a/lib/components/Library/UserArtists.dart +++ b/lib/components/Library/UserArtists.dart @@ -49,15 +49,19 @@ class UserArtists extends HookConsumerWidget { ), padding: const EdgeInsets.all(10), itemBuilder: (context, index) { - if (index == artists.length - 1 && hasNextPage) { - return Waypoint( - onEnter: () { - artistQuery.fetchNextPage(); - }, - child: ArtistCard(artists[index]), - ); - } - return ArtistCard(artists[index]); + return HookBuilder(builder: (context) { + if (index == artists.length - 1 && hasNextPage) { + return Waypoint( + controller: useScrollController(), + isGrid: true, + onTouchEdge: () { + artistQuery.fetchNextPage(); + }, + child: ArtistCard(artists[index]), + ); + } + return ArtistCard(artists[index]); + }); }, ), ); diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index b25847508..cd82ccc4b 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -200,26 +200,24 @@ class Search extends HookConsumerWidget { if (playlists.isNotEmpty) PlatformText.headline("Playlists"), const SizedBox(height: 10), - if (searchPlaylist.isLoading && - !searchPlaylist.isFetchingNextPage) - const PlatformCircularProgressIndicator() - else if (searchPlaylist.hasError) - PlatformText(searchPlaylist - .error?[searchPlaylist.pageParams.last]) - else - ScrollConfiguration( - behavior: ScrollConfiguration.of(context) - .copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, + ScrollConfiguration( + behavior: + ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + scrollbarOrientation: + breakpoint > Breakpoints.md + ? ScrollbarOrientation.bottom + : ScrollbarOrientation.top, + controller: playlistController, + child: Waypoint( + onTouchEdge: () { + searchPlaylist.fetchNextPage(); }, - ), - child: Scrollbar( - scrollbarOrientation: - breakpoint > Breakpoints.md - ? ScrollbarOrientation.bottom - : ScrollbarOrientation.top, controller: playlistController, child: SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -231,15 +229,8 @@ class Search extends HookConsumerWidget { if (i == playlists.length - 1 && searchPlaylist .hasNextPage) { - return Waypoint( - onEnter: () { - searchPlaylist - .fetchNextPage(); - }, - child: - const ShimmerPlaybuttonCard( - count: 1), - ); + return const ShimmerPlaybuttonCard( + count: 1); } return PlaylistCard(playlist); }, @@ -249,27 +240,32 @@ class Search extends HookConsumerWidget { ), ), ), + ), + if (searchPlaylist.isLoading && + !searchPlaylist.isFetchingNextPage) + const PlatformCircularProgressIndicator(), + if (searchPlaylist.hasError) + PlatformText(searchPlaylist + .error?[searchPlaylist.pageParams.last]), const SizedBox(height: 20), if (artists.isNotEmpty) PlatformText.headline("Artists"), const SizedBox(height: 10), - if (searchArtist.isLoading && - !searchArtist.isFetchingNextPage) - const PlatformCircularProgressIndicator() - else if (searchArtist.hasError) - PlatformText(searchArtist - .error?[searchArtist.pageParams.last]) - else - ScrollConfiguration( - behavior: ScrollConfiguration.of(context) - .copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( + ScrollConfiguration( + behavior: + ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + controller: artistController, + child: Waypoint( controller: artistController, + onTouchEdge: () { + searchArtist.fetchNextPage(); + }, child: SingleChildScrollView( scrollDirection: Axis.horizontal, controller: artistController, @@ -279,15 +275,8 @@ class Search extends HookConsumerWidget { (i, artist) { if (i == artists.length - 1 && searchArtist.hasNextPage) { - return Waypoint( - onEnter: () { - searchArtist - .fetchNextPage(); - }, - child: - const ShimmerPlaybuttonCard( - count: 1), - ); + return const ShimmerPlaybuttonCard( + count: 1); } return Container( margin: const EdgeInsets @@ -302,6 +291,13 @@ class Search extends HookConsumerWidget { ), ), ), + ), + if (searchArtist.isLoading && + !searchArtist.isFetchingNextPage) + const PlatformCircularProgressIndicator(), + if (searchArtist.hasError) + PlatformText(searchArtist + .error?[searchArtist.pageParams.last]), const SizedBox(height: 20), if (albums.isNotEmpty) PlatformText( @@ -310,23 +306,21 @@ class Search extends HookConsumerWidget { Theme.of(context).textTheme.headline5, ), const SizedBox(height: 10), - if (searchAlbum.isLoading && - !searchAlbum.isFetchingNextPage) - const PlatformCircularProgressIndicator() - else if (searchAlbum.hasError) - PlatformText(searchAlbum - .error?[searchAlbum.pageParams.last]) - else - ScrollConfiguration( - behavior: ScrollConfiguration.of(context) - .copyWith( - dragDevices: { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }, - ), - child: Scrollbar( + ScrollConfiguration( + behavior: + ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }, + ), + child: Scrollbar( + controller: albumController, + child: Waypoint( controller: albumController, + onTouchEdge: () { + searchAlbum.fetchNextPage(); + }, child: SingleChildScrollView( scrollDirection: Axis.horizontal, controller: albumController, @@ -335,14 +329,8 @@ class Search extends HookConsumerWidget { ...albums.mapIndexed((i, album) { if (i == albums.length - 1 && searchAlbum.hasNextPage) { - return Waypoint( - onEnter: () { - searchAlbum.fetchNextPage(); - }, - child: - const ShimmerPlaybuttonCard( - count: 1), - ); + return const ShimmerPlaybuttonCard( + count: 1); } return AlbumCard( TypeConversionUtils @@ -356,6 +344,13 @@ class Search extends HookConsumerWidget { ), ), ), + ), + if (searchAlbum.isLoading && + !searchAlbum.isFetchingNextPage) + const PlatformCircularProgressIndicator(), + if (searchAlbum.hasError) + PlatformText(searchAlbum + .error?[searchAlbum.pageParams.last]), ], ), ), diff --git a/lib/components/Shared/SpotubePageRoute.dart b/lib/components/Shared/SpotubePageRoute.dart index dd40b4230..48af33b3b 100644 --- a/lib/components/Shared/SpotubePageRoute.dart +++ b/lib/components/Shared/SpotubePageRoute.dart @@ -1,18 +1,27 @@ -import 'package:flutter/material.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:go_router/go_router.dart'; +import 'package:platform_ui/platform_ui.dart'; -class SpotubePageRoute extends PageRouteBuilder { - final Widget child; - SpotubePageRoute({required this.child}) - : super( - pageBuilder: (context, animation, secondaryAnimation) => child, - settings: RouteSettings( - name: child.key.toString(), - ), +class SpotubePage extends CustomTransitionPage { + SpotubePage({ + required super.child, + }) : super( + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return child; + }, ); -} -class SpotubePage extends MaterialPage { - const SpotubePage({ - required Widget child, - }) : super(child: child); + @override + Route createRoute(BuildContext context) { + if (platform == TargetPlatform.windows) { + return FluentPageRoute( + builder: (context) => child, + settings: this, + maintainState: maintainState, + barrierLabel: barrierLabel, + fullscreenDialog: fullscreenDialog, + ); + } + return super.createRoute(context); + } } diff --git a/lib/components/Shared/Waypoint.dart b/lib/components/Shared/Waypoint.dart index 68e128e3f..128d19aba 100644 --- a/lib/components/Shared/Waypoint.dart +++ b/lib/components/Shared/Waypoint.dart @@ -1,29 +1,57 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:visibility_detector/visibility_detector.dart'; -class Waypoint extends StatelessWidget { - final void Function()? onEnter; - final void Function()? onLeave; +class Waypoint extends HookWidget { + final void Function()? onTouchEdge; final Widget? child; + final ScrollController controller; + final bool isGrid; + const Waypoint({ Key? key, - this.onEnter, - this.onLeave, + required this.controller, + this.isGrid = false, + this.onTouchEdge, 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(); + useEffect(() { + if (isGrid) { + return null; + } + listener() { + // nextPageTrigger will have a value equivalent to 80% of the list size. + final nextPageTrigger = 0.8 * controller.position.maxScrollExtent; + +// scrollController fetches the next paginated data when the current postion of the user on the screen has surpassed + if (controller.position.pixels >= nextPageTrigger) { + onTouchEdge?.call(); } - }, - child: child ?? Container(), - ); + } + + if (controller.hasClients) { + listener(); + } + + controller.addListener(listener); + return () => controller.removeListener(listener); + }, [controller, onTouchEdge]); + + if (isGrid) { + return VisibilityDetector( + key: const Key("waypoint"), + onVisibilityChanged: (info) { + if (info.visibleFraction > 0) { + onTouchEdge?.call(); + } + }, + child: child ?? Container(), + ); + } + + return child ?? Container(); } } diff --git a/lib/models/GoRouteDeclarations.dart b/lib/models/GoRouteDeclarations.dart index c616c4b6e..bd4e5b958 100644 --- a/lib/models/GoRouteDeclarations.dart +++ b/lib/models/GoRouteDeclarations.dart @@ -28,28 +28,28 @@ final router = GoRouter( routes: [ GoRoute( path: "/", - pageBuilder: (context, state) => const SpotubePage(child: Genres()), + pageBuilder: (context, state) => SpotubePage(child: const Genres()), ), GoRoute( path: "/search", name: "Search", - pageBuilder: (context, state) => const SpotubePage(child: Search()), + pageBuilder: (context, state) => SpotubePage(child: const Search()), ), GoRoute( path: "/library", name: "Library", pageBuilder: (context, state) => - const SpotubePage(child: UserLibrary()), + SpotubePage(child: const UserLibrary()), ), GoRoute( path: "/lyrics", name: "Lyrics", - pageBuilder: (context, state) => const SpotubePage(child: Lyrics()), + pageBuilder: (context, state) => SpotubePage(child: const Lyrics()), ), GoRoute( path: "/settings", - pageBuilder: (context, state) => const SpotubePage( - child: Settings(), + pageBuilder: (context, state) => SpotubePage( + child: const Settings(), ), ), GoRoute( @@ -87,16 +87,16 @@ final router = GoRouter( GoRoute( path: "/login-tutorial", parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => const SpotubePage( - child: LoginTutorial(), + pageBuilder: (context, state) => SpotubePage( + child: const LoginTutorial(), ), ), GoRoute( path: "/player", parentNavigatorKey: rootNavigatorKey, pageBuilder: (context, state) { - return const SpotubePage( - child: PlayerView(), + return SpotubePage( + child: const PlayerView(), ); }, ), diff --git a/pubspec.lock b/pubspec.lock index 9a473fa0a..645a552d6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1050,11 +1050,9 @@ packages: platform_ui: dependency: "direct main" description: - path: "." - ref: bf42bc4caf9cb382f5215ea2db711adbf2a99f4b - resolved-ref: bf42bc4caf9cb382f5215ea2db711adbf2a99f4b - url: "https://github.com/KRTirtho/platform_ui.git" - source: git + path: "../platform_ui" + relative: true + source: path version: "0.1.0" plugin_platform_interface: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index c9f4a37e4..bce3be6f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,9 +63,7 @@ dependencies: tuple: ^2.0.1 uuid: ^3.0.6 platform_ui: - git: - url: https://github.com/KRTirtho/platform_ui.git - ref: bf42bc4caf9cb382f5215ea2db711adbf2a99f4b + path: ../platform_ui fluent_ui: ^4.0.3 macos_ui: ^1.7.5 libadwaita: ^1.2.5