fix: horizontal infinite lists doesn't fill the screen

This commit is contained in:
Kingkor Roy Tirtho 2022-12-07 10:27:46 +06:00
parent 067e9ac53e
commit 69995bea1c
10 changed files with 212 additions and 175 deletions

View File

@ -50,21 +50,22 @@ class ArtistAlbumList extends HookConsumerWidget {
child: Scrollbar( child: Scrollbar(
interactive: false, interactive: false,
controller: scrollController, controller: scrollController,
child: ListView.builder( child: Waypoint(
itemCount: albums.length,
controller: scrollController, controller: scrollController,
scrollDirection: Axis.horizontal, onTouchEdge: () {
itemBuilder: (context, index) { albumsQuery.fetchNextPage();
if (index == albums.length - 1 && hasNextPage) {
return Waypoint(
onEnter: () {
albumsQuery.fetchNextPage();
},
child: const ShimmerPlaybuttonCard(count: 1),
);
}
return AlbumCard(albums[index]);
}, },
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]);
},
),
), ),
), ),
), ),

View File

@ -66,21 +66,22 @@ class CategoryCard extends HookConsumerWidget {
child: Scrollbar( child: Scrollbar(
controller: scrollController, controller: scrollController,
interactive: false, interactive: false,
child: ListView.builder( child: Waypoint(
scrollDirection: Axis.horizontal, controller: scrollController,
shrinkWrap: true, onTouchEdge: () {
itemCount: playlists.length, playlistQuery.fetchNextPage();
itemBuilder: (context, index) {
if (index == playlists.length - 1 && hasNextPage) {
return Waypoint(
onEnter: () {
playlistQuery.fetchNextPage();
},
child: const ShimmerPlaybuttonCard(count: 1),
);
}
return PlaylistCard(playlists[index]);
}, },
child: ListView(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
controller: scrollController,
children: [
...playlists
.map((playlist) => PlaylistCard(playlist)),
if (hasNextPage)
const ShimmerPlaybuttonCard(count: 1),
],
),
), ),
), ),
), ),

View File

@ -18,6 +18,7 @@ class Genres extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final scrollController = useScrollController();
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final recommendationMarket = ref.watch( final recommendationMarket = ref.watch(
userPreferencesProvider.select((s) => s.recommendationMarket), userPreferencesProvider.select((s) => s.recommendationMarket),
@ -45,23 +46,25 @@ class Genres extends HookConsumerWidget {
return PlatformScaffold( return PlatformScaffold(
appBar: kIsDesktop ? PageWindowTitleBar() : null, appBar: kIsDesktop ? PageWindowTitleBar() : null,
body: ListView.builder( body: Waypoint(
itemCount: categories.length, onTouchEdge: () {
itemBuilder: (context, index) { if (categoriesQuery.hasNextPage) {
final category = categories[index]; categoriesQuery.fetchNextPage();
if (category == null) return Container();
if (index == categories.length - 1) {
return Waypoint(
onEnter: () {
if (categoriesQuery.hasNextPage) {
categoriesQuery.fetchNextPage();
}
},
child: const ShimmerCategories(),
);
} }
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);
},
),
), ),
); );
} }

View File

@ -49,15 +49,19 @@ class UserArtists extends HookConsumerWidget {
), ),
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == artists.length - 1 && hasNextPage) { return HookBuilder(builder: (context) {
return Waypoint( if (index == artists.length - 1 && hasNextPage) {
onEnter: () { return Waypoint(
artistQuery.fetchNextPage(); controller: useScrollController(),
}, isGrid: true,
child: ArtistCard(artists[index]), onTouchEdge: () {
); artistQuery.fetchNextPage();
} },
return ArtistCard(artists[index]); child: ArtistCard(artists[index]),
);
}
return ArtistCard(artists[index]);
});
}, },
), ),
); );

View File

@ -200,26 +200,24 @@ class Search extends HookConsumerWidget {
if (playlists.isNotEmpty) if (playlists.isNotEmpty)
PlatformText.headline("Playlists"), PlatformText.headline("Playlists"),
const SizedBox(height: 10), const SizedBox(height: 10),
if (searchPlaylist.isLoading && ScrollConfiguration(
!searchPlaylist.isFetchingNextPage) behavior:
const PlatformCircularProgressIndicator() ScrollConfiguration.of(context).copyWith(
else if (searchPlaylist.hasError) dragDevices: {
PlatformText(searchPlaylist PointerDeviceKind.touch,
.error?[searchPlaylist.pageParams.last]) PointerDeviceKind.mouse,
else },
ScrollConfiguration( ),
behavior: ScrollConfiguration.of(context) child: Scrollbar(
.copyWith( scrollbarOrientation:
dragDevices: { breakpoint > Breakpoints.md
PointerDeviceKind.touch, ? ScrollbarOrientation.bottom
PointerDeviceKind.mouse, : ScrollbarOrientation.top,
controller: playlistController,
child: Waypoint(
onTouchEdge: () {
searchPlaylist.fetchNextPage();
}, },
),
child: Scrollbar(
scrollbarOrientation:
breakpoint > Breakpoints.md
? ScrollbarOrientation.bottom
: ScrollbarOrientation.top,
controller: playlistController, controller: playlistController,
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -231,15 +229,8 @@ class Search extends HookConsumerWidget {
if (i == playlists.length - 1 && if (i == playlists.length - 1 &&
searchPlaylist searchPlaylist
.hasNextPage) { .hasNextPage) {
return Waypoint( return const ShimmerPlaybuttonCard(
onEnter: () { count: 1);
searchPlaylist
.fetchNextPage();
},
child:
const ShimmerPlaybuttonCard(
count: 1),
);
} }
return PlaylistCard(playlist); 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), const SizedBox(height: 20),
if (artists.isNotEmpty) if (artists.isNotEmpty)
PlatformText.headline("Artists"), PlatformText.headline("Artists"),
const SizedBox(height: 10), const SizedBox(height: 10),
if (searchArtist.isLoading && ScrollConfiguration(
!searchArtist.isFetchingNextPage) behavior:
const PlatformCircularProgressIndicator() ScrollConfiguration.of(context).copyWith(
else if (searchArtist.hasError) dragDevices: {
PlatformText(searchArtist PointerDeviceKind.touch,
.error?[searchArtist.pageParams.last]) PointerDeviceKind.mouse,
else },
ScrollConfiguration( ),
behavior: ScrollConfiguration.of(context) child: Scrollbar(
.copyWith( controller: artistController,
dragDevices: { child: Waypoint(
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: artistController, controller: artistController,
onTouchEdge: () {
searchArtist.fetchNextPage();
},
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
controller: artistController, controller: artistController,
@ -279,15 +275,8 @@ class Search extends HookConsumerWidget {
(i, artist) { (i, artist) {
if (i == artists.length - 1 && if (i == artists.length - 1 &&
searchArtist.hasNextPage) { searchArtist.hasNextPage) {
return Waypoint( return const ShimmerPlaybuttonCard(
onEnter: () { count: 1);
searchArtist
.fetchNextPage();
},
child:
const ShimmerPlaybuttonCard(
count: 1),
);
} }
return Container( return Container(
margin: const EdgeInsets 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), const SizedBox(height: 20),
if (albums.isNotEmpty) if (albums.isNotEmpty)
PlatformText( PlatformText(
@ -310,23 +306,21 @@ class Search extends HookConsumerWidget {
Theme.of(context).textTheme.headline5, Theme.of(context).textTheme.headline5,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
if (searchAlbum.isLoading && ScrollConfiguration(
!searchAlbum.isFetchingNextPage) behavior:
const PlatformCircularProgressIndicator() ScrollConfiguration.of(context).copyWith(
else if (searchAlbum.hasError) dragDevices: {
PlatformText(searchAlbum PointerDeviceKind.touch,
.error?[searchAlbum.pageParams.last]) PointerDeviceKind.mouse,
else },
ScrollConfiguration( ),
behavior: ScrollConfiguration.of(context) child: Scrollbar(
.copyWith( controller: albumController,
dragDevices: { child: Waypoint(
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: albumController, controller: albumController,
onTouchEdge: () {
searchAlbum.fetchNextPage();
},
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
controller: albumController, controller: albumController,
@ -335,14 +329,8 @@ class Search extends HookConsumerWidget {
...albums.mapIndexed((i, album) { ...albums.mapIndexed((i, album) {
if (i == albums.length - 1 && if (i == albums.length - 1 &&
searchAlbum.hasNextPage) { searchAlbum.hasNextPage) {
return Waypoint( return const ShimmerPlaybuttonCard(
onEnter: () { count: 1);
searchAlbum.fetchNextPage();
},
child:
const ShimmerPlaybuttonCard(
count: 1),
);
} }
return AlbumCard( return AlbumCard(
TypeConversionUtils 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]),
], ],
), ),
), ),

View File

@ -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 { class SpotubePage extends CustomTransitionPage {
final Widget child; SpotubePage({
SpotubePageRoute({required this.child}) required super.child,
: super( }) : super(
pageBuilder: (context, animation, secondaryAnimation) => child, transitionsBuilder: (context, animation, secondaryAnimation, child) {
settings: RouteSettings( return child;
name: child.key.toString(), },
),
); );
}
class SpotubePage extends MaterialPage { @override
const SpotubePage({ Route createRoute(BuildContext context) {
required Widget child, if (platform == TargetPlatform.windows) {
}) : super(child: child); return FluentPageRoute(
builder: (context) => child,
settings: this,
maintainState: maintainState,
barrierLabel: barrierLabel,
fullscreenDialog: fullscreenDialog,
);
}
return super.createRoute(context);
}
} }

View File

@ -1,29 +1,57 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
class Waypoint extends StatelessWidget { class Waypoint extends HookWidget {
final void Function()? onEnter; final void Function()? onTouchEdge;
final void Function()? onLeave;
final Widget? child; final Widget? child;
final ScrollController controller;
final bool isGrid;
const Waypoint({ const Waypoint({
Key? key, Key? key,
this.onEnter, required this.controller,
this.onLeave, this.isGrid = false,
this.onTouchEdge,
this.child, this.child,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return VisibilityDetector( useEffect(() {
key: const Key("waypoint"), if (isGrid) {
onVisibilityChanged: (info) { return null;
if (info.visibleFraction == 0) { }
onLeave?.call(); listener() {
} else if (info.visibleFraction > 0) { // nextPageTrigger will have a value equivalent to 80% of the list size.
onEnter?.call(); 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();
} }
} }

View File

@ -28,28 +28,28 @@ final router = GoRouter(
routes: [ routes: [
GoRoute( GoRoute(
path: "/", path: "/",
pageBuilder: (context, state) => const SpotubePage(child: Genres()), pageBuilder: (context, state) => SpotubePage(child: const Genres()),
), ),
GoRoute( GoRoute(
path: "/search", path: "/search",
name: "Search", name: "Search",
pageBuilder: (context, state) => const SpotubePage(child: Search()), pageBuilder: (context, state) => SpotubePage(child: const Search()),
), ),
GoRoute( GoRoute(
path: "/library", path: "/library",
name: "Library", name: "Library",
pageBuilder: (context, state) => pageBuilder: (context, state) =>
const SpotubePage(child: UserLibrary()), SpotubePage(child: const UserLibrary()),
), ),
GoRoute( GoRoute(
path: "/lyrics", path: "/lyrics",
name: "Lyrics", name: "Lyrics",
pageBuilder: (context, state) => const SpotubePage(child: Lyrics()), pageBuilder: (context, state) => SpotubePage(child: const Lyrics()),
), ),
GoRoute( GoRoute(
path: "/settings", path: "/settings",
pageBuilder: (context, state) => const SpotubePage( pageBuilder: (context, state) => SpotubePage(
child: Settings(), child: const Settings(),
), ),
), ),
GoRoute( GoRoute(
@ -87,16 +87,16 @@ final router = GoRouter(
GoRoute( GoRoute(
path: "/login-tutorial", path: "/login-tutorial",
parentNavigatorKey: rootNavigatorKey, parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) => const SpotubePage( pageBuilder: (context, state) => SpotubePage(
child: LoginTutorial(), child: const LoginTutorial(),
), ),
), ),
GoRoute( GoRoute(
path: "/player", path: "/player",
parentNavigatorKey: rootNavigatorKey, parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) { pageBuilder: (context, state) {
return const SpotubePage( return SpotubePage(
child: PlayerView(), child: const PlayerView(),
); );
}, },
), ),

View File

@ -1050,11 +1050,9 @@ packages:
platform_ui: platform_ui:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "../platform_ui"
ref: bf42bc4caf9cb382f5215ea2db711adbf2a99f4b relative: true
resolved-ref: bf42bc4caf9cb382f5215ea2db711adbf2a99f4b source: path
url: "https://github.com/KRTirtho/platform_ui.git"
source: git
version: "0.1.0" version: "0.1.0"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive

View File

@ -63,9 +63,7 @@ dependencies:
tuple: ^2.0.1 tuple: ^2.0.1
uuid: ^3.0.6 uuid: ^3.0.6
platform_ui: platform_ui:
git: path: ../platform_ui
url: https://github.com/KRTirtho/platform_ui.git
ref: bf42bc4caf9cb382f5215ea2db711adbf2a99f4b
fluent_ui: ^4.0.3 fluent_ui: ^4.0.3
macos_ui: ^1.7.5 macos_ui: ^1.7.5
libadwaita: ^1.2.5 libadwaita: ^1.2.5