From 3afe3cea80eb8a662190035519cbff804a81e54e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 14 Sep 2024 10:48:39 +0600 Subject: [PATCH] Squashed commit of the following: commit e160d4f561ff2e945fd67bf12b223c012da58b1e Author: Kingkor Roy Tirtho Date: Sat Sep 14 10:48:08 2024 +0600 fix: pagination issues in playlist and album pages --- .../sections/body/track_view_body.dart | 103 +++++++++--------- lib/provider/history/top/albums.dart | 14 ++- lib/provider/history/top/playlists.dart | 14 ++- lib/provider/history/top/tracks.dart | 14 ++- lib/provider/spotify/album/tracks.dart | 16 ++- lib/provider/spotify/artist/albums.dart | 16 ++- lib/provider/spotify/category/playlists.dart | 16 ++- lib/provider/spotify/playlist/tracks.dart | 14 ++- lib/provider/spotify/search/search.dart | 24 +++- .../utils/provider/paginated_family.dart | 47 ++++---- pubspec.lock | 8 ++ pubspec.yaml | 1 + 12 files changed, 178 insertions(+), 109 deletions(-) diff --git a/lib/components/tracks_view/sections/body/track_view_body.dart b/lib/components/tracks_view/sections/body/track_view_body.dart index df841b8d..faba247a 100644 --- a/lib/components/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/tracks_view/sections/body/track_view_body.dart @@ -65,6 +65,56 @@ class TrackViewBodySection extends HookConsumerWidget { final isActive = playlist.collections.contains(props.collectionId); + final onTapTrackTile = useCallback((Track track, int index) async { + if (trackViewState.isSelecting) { + trackViewState.toggleTrackSelection(track.id!); + return; + } + + final isRemoteDevice = await showSelectDeviceDialog(context, ref); + + if (isRemoteDevice) { + final remotePlayback = ref.read(connectProvider.notifier); + final remoteQueue = ref.read(queueProvider); + if (remoteQueue.collections.contains(props.collectionId) || + remoteQueue.tracks.any((s) => s.id == track.id)) { + await playlistNotifier.jumpToTrack(track); + } else { + final tracks = await props.pagination.onFetchAll(); + await remotePlayback.load( + props.collection is AlbumSimple + ? WebSocketLoadEventData.album( + tracks: tracks, + collection: props.collection as AlbumSimple, + initialIndex: index, + ) + : WebSocketLoadEventData.playlist( + tracks: tracks, + collection: props.collection as PlaylistSimple, + initialIndex: index, + ), + ); + } + } else { + if (isActive || playlist.tracks.contains(track)) { + await playlistNotifier.jumpToTrack(track); + } else { + final tracks = await props.pagination.onFetchAll(); + await playlistNotifier.load( + tracks, + initialIndex: index, + autoPlay: true, + ); + playlistNotifier.addCollection(props.collectionId); + if (props.collection is AlbumSimple) { + historyNotifier.addAlbums([props.collection as AlbumSimple]); + } else { + historyNotifier.addPlaylists([props.collection as PlaylistSimple]); + } + } + } + }, [isActive, playlist, props, playlistNotifier, historyNotifier]); + return SliverMainAxisGroup( slivers: [ SliverToBoxAdapter( @@ -130,58 +180,7 @@ class TrackViewBodySection extends HookConsumerWidget { trackViewState.selectTrack(track.id!); HapticFeedback.selectionClick(); }, - onTap: () async { - if (trackViewState.isSelecting) { - trackViewState.toggleTrackSelection(track.id!); - return; - } - - final isRemoteDevice = - await showSelectDeviceDialog(context, ref); - - if (isRemoteDevice) { - final remotePlayback = ref.read(connectProvider.notifier); - final remoteQueue = ref.read(queueProvider); - if (remoteQueue.collections.contains(props.collectionId) || - remoteQueue.tracks.any((s) => s.id == track.id)) { - await playlistNotifier.jumpToTrack(track); - } else { - final tracks = await props.pagination.onFetchAll(); - await remotePlayback.load( - props.collection is AlbumSimple - ? WebSocketLoadEventData.album( - tracks: tracks, - collection: props.collection as AlbumSimple, - initialIndex: index, - ) - : WebSocketLoadEventData.playlist( - tracks: tracks, - collection: props.collection as PlaylistSimple, - initialIndex: index, - ), - ); - } - } else { - if (isActive || playlist.tracks.contains(track)) { - await playlistNotifier.jumpToTrack(track); - } else { - final tracks = await props.pagination.onFetchAll(); - await playlistNotifier.load( - tracks, - initialIndex: index, - autoPlay: true, - ); - playlistNotifier.addCollection(props.collectionId); - if (props.collection is AlbumSimple) { - historyNotifier - .addAlbums([props.collection as AlbumSimple]); - } else { - historyNotifier - .addPlaylists([props.collection as PlaylistSimple]); - } - } - } - }, + onTap: () => onTapTrackTile(track, index), ); }, ), diff --git a/lib/provider/history/top/albums.dart b/lib/provider/history/top/albums.dart index 7448a849..b11e62d2 100644 --- a/lib/provider/history/top/albums.dart +++ b/lib/provider/history/top/albums.dart @@ -90,12 +90,18 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< fetch(arg, offset, limit) async { final albumsQuery = createAlbumsQuery(limit: limit, offset: offset); - return getAlbumsWithCount(await albumsQuery.get()); + final items = getAlbumsWithCount(await albumsQuery.get()); + + return ( + items: items, + hasMore: items.length == limit, + nextOffset: offset + limit, + ); } @override build(arg) async { - final albums = await fetch(arg, 0, 20); + final (items: albums, :hasMore, :nextOffset) = await fetch(arg, 0, 20); final subscription = createAlbumsQuery().watch().listen((event) { if (state.asData == null) return; @@ -111,9 +117,9 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< return HistoryTopAlbumsState( items: albums, - offset: albums.length, + offset: nextOffset, limit: 20, - hasMore: true, + hasMore: hasMore, ); } diff --git a/lib/provider/history/top/playlists.dart b/lib/provider/history/top/playlists.dart index 04071f7a..19eb3622 100644 --- a/lib/provider/history/top/playlists.dart +++ b/lib/provider/history/top/playlists.dart @@ -55,12 +55,18 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier< fetch(arg, offset, limit) async { final playlistsQuery = createPlaylistsQuery()..limit(limit, offset: offset); - return getPlaylistsWithCount(await playlistsQuery.get()); + final items = getPlaylistsWithCount(await playlistsQuery.get()); + + return ( + items: items, + hasMore: items.length == limit, + nextOffset: offset + limit, + ); } @override build(arg) async { - final playlists = await fetch(arg, 0, 20); + final (items: playlists, :hasMore, :nextOffset) = await fetch(arg, 0, 20); final subscription = createPlaylistsQuery().watch().listen((event) { if (state.asData == null) return; @@ -76,9 +82,9 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier< return HistoryTopPlaylistsState( items: playlists, - offset: playlists.length, + offset: nextOffset, limit: 20, - hasMore: true, + hasMore: hasMore, ); } diff --git a/lib/provider/history/top/tracks.dart b/lib/provider/history/top/tracks.dart index 56795cc6..b737d148 100644 --- a/lib/provider/history/top/tracks.dart +++ b/lib/provider/history/top/tracks.dart @@ -89,12 +89,18 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< fetch(arg, offset, limit) async { final tracksQuery = createTracksQuery()..limit(limit, offset: offset); - return getTracksWithCount(await tracksQuery.get()); + final items = getTracksWithCount(await tracksQuery.get()); + + return ( + items: items, + hasMore: items.length == limit, + nextOffset: offset + limit, + ); } @override build(arg) async { - final tracks = await fetch(arg, 0, 20); + final (items: tracks, :hasMore, :nextOffset) = await fetch(arg, 0, 20); final subscription = createTracksQuery().watch().listen((event) { if (state.asData == null) return; @@ -110,9 +116,9 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< return HistoryTopTracksState( items: tracks, - offset: tracks.length, + offset: nextOffset, limit: 20, - hasMore: true, + hasMore: hasMore, ); } diff --git a/lib/provider/spotify/album/tracks.dart b/lib/provider/spotify/album/tracks.dart index e9f712e7..e39abad5 100644 --- a/lib/provider/spotify/album/tracks.dart +++ b/lib/provider/spotify/album/tracks.dart @@ -31,7 +31,13 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier e.asTrack(arg)).toList() ?? []; + final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? []; + + return ( + items: items, + hasMore: !tracks.isLast, + nextOffset: tracks.nextOffset, + ); } @override @@ -39,12 +45,12 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier s.market), ); - final albums = await fetch(arg, 0, 20); + final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 20); return ArtistAlbumsState( - items: albums, - offset: 0, + items: items, + offset: nextOffset, limit: 20, - hasMore: albums.length == 20, + hasMore: hasMore, ); } } diff --git a/lib/provider/spotify/category/playlists.dart b/lib/provider/spotify/category/playlists.dart index 18d4845f..9f1034be 100644 --- a/lib/provider/spotify/category/playlists.dart +++ b/lib/provider/spotify/category/playlists.dart @@ -39,7 +39,13 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< (json) => PlaylistsFeatured.fromJson(json), ).getPage(limit, offset); - return playlists.items?.whereNotNull().toList() ?? []; + final items = playlists.items?.whereNotNull().toList() ?? []; + + return ( + items: items, + hasMore: !playlists.isLast, + nextOffset: playlists.nextOffset, + ); } @override @@ -50,13 +56,13 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< ref.watch(userPreferencesProvider.select((s) => s.locale)); ref.watch(userPreferencesProvider.select((s) => s.market)); - final playlists = await fetch(arg, 0, 8); + final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 8); return CategoryPlaylistsState( - items: playlists, - offset: 0, + items: items, + offset: nextOffset, limit: 8, - hasMore: playlists.length == 8, + hasMore: hasMore, ); } } diff --git a/lib/provider/spotify/playlist/tracks.dart b/lib/provider/spotify/playlist/tracks.dart index 1803f6fc..379ad110 100644 --- a/lib/provider/spotify/playlist/tracks.dart +++ b/lib/provider/spotify/playlist/tracks.dart @@ -36,10 +36,16 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< /// Filter out tracks with null id because some personal playlists /// may contain local tracks that are not available in the Spotify catalog - return tracks.items + final items = tracks.items ?.where((track) => track.id != null && track.type == "track") .toList() ?? []; + + return ( + items: items, + hasMore: !tracks.isLast, + nextOffset: tracks.nextOffset, + ); } @override @@ -47,13 +53,13 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< ref.cacheFor(); ref.watch(spotifyProvider); - final tracks = await fetch(arg, 0, 20); + final (items: tracks, :hasMore, :nextOffset) = await fetch(arg, 0, 20); return PlaylistTracksState( items: tracks, - offset: 0, + offset: nextOffset, limit: 20, - hasMore: tracks.length == 20, + hasMore: hasMore, ); } } diff --git a/lib/provider/spotify/search/search.dart b/lib/provider/spotify/search/search.dart index dc00d913..5bbc02e4 100644 --- a/lib/provider/spotify/search/search.dart +++ b/lib/provider/spotify/search/search.dart @@ -37,7 +37,13 @@ class SearchNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier[], + hasMore: false, + nextOffset: 0, + ); + } final results = await spotify.search .get( ref.read(searchTermStateProvider), @@ -46,7 +52,13 @@ class SearchNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier e.items ?? []).toList().cast(); + final items = results.expand((e) => e.items ?? []).toList().cast(); + + return ( + items: items, + hasMore: items.length == limit, + nextOffset: offset + limit, + ); } @override @@ -59,13 +71,13 @@ class SearchNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier value.market), ); - final results = await fetch(arg, 0, 10); + final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 10); return SearchState( - items: results, - offset: 0, + items: items, + offset: nextOffset, limit: 10, - hasMore: results.length == 10, + hasMore: hasMore, ); } } diff --git a/lib/provider/spotify/utils/provider/paginated_family.dart b/lib/provider/spotify/utils/provider/paginated_family.dart index 84c6ba20..c08c8673 100644 --- a/lib/provider/spotify/utils/provider/paginated_family.dart +++ b/lib/provider/spotify/utils/provider/paginated_family.dart @@ -1,10 +1,16 @@ part of '../../spotify.dart'; +typedef PseudoPaginatedProps = ({ + List items, + int nextOffset, + bool hasMore, +}); + abstract class FamilyPaginatedAsyncNotifier< K, T extends BasePaginatedState, A> extends FamilyAsyncNotifier with SpotifyMixin { - Future> fetch(A arg, int offset, int limit); + Future> fetch(A arg, int offset, int limit); Future fetchMore() async { if (state.value == null || !state.value!.hasMore) return; @@ -13,18 +19,18 @@ abstract class FamilyPaginatedAsyncNotifier< state = await AsyncValue.guard( () async { - final items = await fetch( + final (:items, :hasMore, :nextOffset) = await fetch( arg, - state.value!.offset + state.value!.limit, + state.value!.offset, state.value!.limit, ); return state.value!.copyWith( - hasMore: items.length == state.value!.limit, + hasMore: hasMore, items: [ ...state.value!.items, ...items, ], - offset: state.value!.offset + state.value!.limit, + offset: nextOffset, ) as T; }, ); @@ -37,16 +43,16 @@ abstract class FamilyPaginatedAsyncNotifier< bool hasMore = true; while (hasMore) { await update((state) async { - final items = await fetch( + final res = await fetch( arg, - state.offset + state.limit, + state.offset, state.limit, ); - hasMore = items.length == state.limit; + hasMore = res.hasMore; return state.copyWith( - items: [...state.items, ...items], - offset: state.offset + state.limit, + items: [...state.items, ...res.items], + offset: res.nextOffset, hasMore: hasMore, ) as T; }); @@ -60,7 +66,7 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier< K, T extends BasePaginatedState, A> extends AutoDisposeFamilyAsyncNotifier with SpotifyMixin { - Future> fetch(A arg, int offset, int limit); + Future> fetch(A arg, int offset, int limit); Future fetchMore() async { if (state.value == null || !state.value!.hasMore) return; @@ -69,18 +75,19 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier< state = await AsyncValue.guard( () async { - final items = await fetch( + final (:items, :hasMore, :nextOffset) = await fetch( arg, - state.value!.offset + state.value!.limit, + state.value!.offset, state.value!.limit, ); + return state.value!.copyWith( - hasMore: items.length == state.value!.limit, + hasMore: hasMore, items: [ ...state.value!.items, ...items, ], - offset: state.value!.offset + state.value!.limit, + offset: nextOffset, ) as T; }, ); @@ -93,16 +100,16 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier< bool hasMore = true; while (hasMore) { await update((state) async { - final items = await fetch( + final res = await fetch( arg, - state.offset + state.limit, + state.offset, state.limit, ); - hasMore = items.length == state.limit; + hasMore = res.hasMore; return state.copyWith( - items: [...state.items, ...items], - offset: state.offset + state.limit, + items: [...state.items, ...res.items], + offset: res.nextOffset, hasMore: hasMore, ) as T; }); diff --git a/pubspec.lock b/pubspec.lock index 3249c759..a1494a2d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -758,6 +758,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.5" + flutter_hooks_lint: + dependency: "direct dev" + description: + name: flutter_hooks_lint + sha256: fc6e18505b597737e5d620656e340ac60e7a58980cca29e18c1216bd15083674 + url: "https://pub.dev" + source: hosted + version: "1.2.0" flutter_inappwebview: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d69ab5db..402cd474 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -158,6 +158,7 @@ dev_dependencies: xml: ^6.5.0 io: ^1.0.4 drift_dev: ^2.18.0 + flutter_hooks_lint: ^1.2.0 dependency_overrides: uuid: ^4.4.0