Squashed commit of the following:

commit e160d4f561ff2e945fd67bf12b223c012da58b1e
Author: Kingkor Roy Tirtho <krtirtho@gmail.com>
Date:   Sat Sep 14 10:48:08 2024 +0600

    fix: pagination issues in playlist and album pages
This commit is contained in:
Kingkor Roy Tirtho 2024-09-14 10:48:39 +06:00
parent 40bfcc1961
commit 3afe3cea80
12 changed files with 178 additions and 109 deletions

View File

@ -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),
);
},
),

View File

@ -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,
);
}

View File

@ -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,
);
}

View File

@ -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,
);
}

View File

@ -31,7 +31,13 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
@override
fetch(arg, offset, limit) async {
final tracks = await spotify.albums.tracks(arg.id!).getPage(limit, offset);
return tracks.items?.map((e) => 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<Track,
ref.cacheFor();
ref.watch(spotifyProvider);
final tracks = await fetch(arg, 0, 20);
final (:items, :nextOffset, :hasMore) = await fetch(arg, 0, 20);
return AlbumTracksState(
items: tracks,
offset: 0,
items: items,
offset: nextOffset,
limit: 20,
hasMore: tracks.length == 20,
hasMore: hasMore,
);
}
}

View File

@ -35,7 +35,13 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
.albums(arg, country: market)
.getPage(limit, offset);
return albums.items?.toList() ?? [];
final items = albums.items?.toList() ?? [];
return (
items: items,
hasMore: !albums.isLast,
nextOffset: albums.nextOffset,
);
}
@override
@ -46,12 +52,12 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
ref.watch(
userPreferencesProvider.select((s) => 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,
);
}
}

View File

@ -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,
);
}
}

View File

@ -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() ??
<Track>[];
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,
);
}
}

View File

@ -37,7 +37,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
@override
fetch(arg, offset, limit) async {
if (state.value == null) return [];
if (state.value == null) {
return (
items: <Y>[],
hasMore: false,
nextOffset: 0,
);
}
final results = await spotify.search
.get(
ref.read(searchTermStateProvider),
@ -46,7 +52,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
)
.getPage(limit, offset);
return results.expand((e) => e.items ?? <Y>[]).toList().cast<Y>();
final items = results.expand((e) => e.items ?? <Y>[]).toList().cast<Y>();
return (
items: items,
hasMore: items.length == limit,
nextOffset: offset + limit,
);
}
@override
@ -59,13 +71,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
userPreferencesProvider.select((value) => value.market),
);
final results = await fetch(arg, 0, 10);
final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 10);
return SearchState<Y>(
items: results,
offset: 0,
items: items,
offset: nextOffset,
limit: 10,
hasMore: results.length == 10,
hasMore: hasMore,
);
}
}

View File

@ -1,10 +1,16 @@
part of '../../spotify.dart';
typedef PseudoPaginatedProps<T> = ({
List<T> items,
int nextOffset,
bool hasMore,
});
abstract class FamilyPaginatedAsyncNotifier<
K,
T extends BasePaginatedState<K, dynamic>,
A> extends FamilyAsyncNotifier<T, A> with SpotifyMixin<T> {
Future<List<K>> fetch(A arg, int offset, int limit);
Future<PseudoPaginatedProps<K>> fetch(A arg, int offset, int limit);
Future<void> 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<K, dynamic>,
A> extends AutoDisposeFamilyAsyncNotifier<T, A> with SpotifyMixin<T> {
Future<List<K>> fetch(A arg, int offset, int limit);
Future<PseudoPaginatedProps<K>> fetch(A arg, int offset, int limit);
Future<void> 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;
});

View File

@ -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:

View File

@ -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