mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Partial async caching support added using FutureProvider
This commit is contained in:
parent
5bfe29b498
commit
20ada95312
@ -4,11 +4,11 @@ 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/Playlist/PlaylistCard.dart';
|
||||
import 'package:spotube/hooks/usePagingController.dart';
|
||||
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
class CategoryCard extends HookWidget {
|
||||
class CategoryCard extends HookConsumerWidget {
|
||||
final Category category;
|
||||
final Iterable<PlaylistSimple>? playlists;
|
||||
CategoryCard(
|
||||
@ -20,7 +20,33 @@ class CategoryCard extends HookWidget {
|
||||
final logger = getLogger(CategoryCard);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final scrollController = useScrollController();
|
||||
final mounted = useIsMounted();
|
||||
|
||||
final pagingController =
|
||||
usePaginatedFutureProvider<Page<PlaylistSimple>, int, PlaylistSimple>(
|
||||
(pageKey) => categoryPlaylistsQuery(
|
||||
[
|
||||
category.id,
|
||||
pageKey,
|
||||
].join("/"),
|
||||
),
|
||||
ref: ref,
|
||||
firstPageKey: 0,
|
||||
onData: (data, pagingController, pageKey) {
|
||||
if (playlists != null && playlists?.isNotEmpty == true && mounted()) {
|
||||
return pagingController.appendLastPage(playlists!.toList());
|
||||
}
|
||||
final page = data.value;
|
||||
if (page.isLast && page.items != null) {
|
||||
pagingController.appendLastPage(page.items!.toList());
|
||||
} else if (page.items != null) {
|
||||
pagingController.appendPage(page.items!.toList(), page.nextOffset);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
@ -34,55 +60,9 @@ class CategoryCard extends HookWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
final scrollController = useScrollController();
|
||||
final pagingController =
|
||||
usePagingController<int, PlaylistSimple>(firstPageKey: 0);
|
||||
|
||||
final _error = useState(false);
|
||||
final mounted = useIsMounted();
|
||||
|
||||
useEffect(() {
|
||||
listener(pageKey) async {
|
||||
try {
|
||||
if (playlists != null &&
|
||||
playlists?.isNotEmpty == true &&
|
||||
mounted()) {
|
||||
return pagingController.appendLastPage(playlists!.toList());
|
||||
}
|
||||
final Page<PlaylistSimple> page = await (category.id !=
|
||||
"user-featured-playlists"
|
||||
? spotifyApi.playlists.getByCategoryId(category.id!)
|
||||
: spotifyApi.playlists.featured)
|
||||
.getPage(3, pageKey);
|
||||
|
||||
if (!mounted()) return;
|
||||
if (page.isLast && page.items != null) {
|
||||
pagingController.appendLastPage(page.items!.toList());
|
||||
} else if (page.items != null) {
|
||||
pagingController.appendPage(
|
||||
page.items!.toList(), page.nextOffset);
|
||||
}
|
||||
if (_error.value) _error.value = false;
|
||||
} catch (e, stack) {
|
||||
if (mounted()) {
|
||||
if (!_error.value) _error.value = true;
|
||||
pagingController.error = e;
|
||||
}
|
||||
logger.e("pagingController.addPageRequestListener", e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
pagingController.addPageRequestListener(listener);
|
||||
return () {
|
||||
pagingController.removePageRequestListener(listener);
|
||||
};
|
||||
}, [_error]);
|
||||
|
||||
if (_error.value) return const Text("Something Went Wrong");
|
||||
return SizedBox(
|
||||
pagingController.error != null
|
||||
? const Text("Something Went Wrong")
|
||||
: SizedBox(
|
||||
height: 245,
|
||||
child: Scrollbar(
|
||||
controller: scrollController,
|
||||
@ -98,8 +78,6 @@ class CategoryCard extends HookWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
|
@ -18,11 +18,9 @@ import 'package:spotube/components/Player/Player.dart';
|
||||
import 'package:spotube/components/Library/UserLibrary.dart';
|
||||
import 'package:spotube/hooks/useBreakpointValue.dart';
|
||||
import 'package:spotube/hooks/useHotKeys.dart';
|
||||
import 'package:spotube/hooks/usePagingController.dart';
|
||||
import 'package:spotube/hooks/useSharedPreferences.dart';
|
||||
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
List<String> spotifyScopes = [
|
||||
"playlist-modify-public",
|
||||
@ -42,12 +40,6 @@ class Home extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
String recommendationMarket = ref.watch(userPreferencesProvider.select(
|
||||
(value) => (value.recommendationMarket),
|
||||
));
|
||||
|
||||
final pagingController =
|
||||
usePagingController<int, Category>(firstPageKey: 0);
|
||||
final int titleBarDragMaxWidth = useBreakpointValue(
|
||||
md: 72,
|
||||
lg: 256,
|
||||
@ -58,53 +50,9 @@ class Home extends HookConsumerWidget {
|
||||
final _selectedIndex = useState(0);
|
||||
_onSelectedIndexChanged(int index) => _selectedIndex.value = index;
|
||||
|
||||
final localStorage = useSharedPreferences();
|
||||
|
||||
// initializing global hot keys
|
||||
useHotKeys(ref);
|
||||
|
||||
final listener = useCallback((int pageKey) async {
|
||||
final spotify = ref.read(spotifyProvider);
|
||||
|
||||
try {
|
||||
Page<Category> categories = await spotify.categories
|
||||
.list(country: recommendationMarket)
|
||||
.getPage(15, 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 && categories.items != null) {
|
||||
pagingController.appendLastPage(items);
|
||||
} else if (categories.items != null) {
|
||||
pagingController.appendPage(items, categories.nextOffset);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
pagingController.error = e;
|
||||
logger.e("pagingController.addPageRequestListener", e, stack);
|
||||
}
|
||||
}, [recommendationMarket]);
|
||||
|
||||
useEffect(() {
|
||||
try {
|
||||
pagingController.addPageRequestListener(listener);
|
||||
// the world is full of surprises and the previously working
|
||||
// fine pageRequestListener now doesn't notify the listeners
|
||||
// automatically after assigning a listener. So doing it manually
|
||||
pagingController.notifyPageRequestListeners(0);
|
||||
} catch (e, stack) {
|
||||
logger.e("initState", e, stack);
|
||||
}
|
||||
return () {
|
||||
pagingController.removePageRequestListener(listener);
|
||||
};
|
||||
}, [localStorage]);
|
||||
|
||||
final titleBarContents = Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Row(
|
||||
@ -160,14 +108,39 @@ class Home extends HookConsumerWidget {
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: PagedListView(
|
||||
child: HookBuilder(builder: (context) {
|
||||
final pagingController = usePaginatedFutureProvider<
|
||||
Page<Category>, int, Category>(
|
||||
(pageKey) => categoriesQuery(pageKey),
|
||||
ref: ref,
|
||||
firstPageKey: 0,
|
||||
onData: (data, pagingController, pageKey) {
|
||||
final categories = data.value;
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
return PagedListView(
|
||||
pagingController: pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Category>(
|
||||
builderDelegate:
|
||||
PagedChildBuilderDelegate<Category>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return CategoryCard(item);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
if (_selectedIndex.value == 1) const Search(),
|
||||
|
@ -1,65 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
|
||||
class UserArtists extends ConsumerStatefulWidget {
|
||||
const UserArtists({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<UserArtists> createState() => _UserArtistsState();
|
||||
}
|
||||
|
||||
class _UserArtistsState extends ConsumerState<UserArtists> {
|
||||
final PagingController<String, Artist> _pagingController =
|
||||
PagingController(firstPageKey: "");
|
||||
class UserArtists extends HookConsumerWidget {
|
||||
UserArtists({Key? key}) : super(key: key);
|
||||
final logger = getLogger(UserArtists);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timestamp) {
|
||||
_pagingController.addPageRequestListener((pageKey) async {
|
||||
try {
|
||||
SpotifyApi spotifyApi = ref.read(spotifyProvider);
|
||||
CursorPage<Artist> artists = await spotifyApi.me
|
||||
.following(FollowingType.artist)
|
||||
.getPage(15, pageKey);
|
||||
|
||||
var items = artists.items!.toList();
|
||||
Widget build(BuildContext context, ref) {
|
||||
final pagingController =
|
||||
usePaginatedFutureProvider<CursorPage<Artist>, String, Artist>(
|
||||
(pageKey) => currentUserFollowingArtistsQuery(pageKey),
|
||||
ref: ref,
|
||||
firstPageKey: "",
|
||||
onData: (data, pagingController, pageKey) {
|
||||
final artists = data.value;
|
||||
final items = artists.items!.toList();
|
||||
|
||||
if (artists.items != null && items.length < 15) {
|
||||
_pagingController.appendLastPage(items);
|
||||
pagingController.appendLastPage(items);
|
||||
} else if (artists.items != null) {
|
||||
_pagingController.appendPage(items, items.last.id);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
_pagingController.error = e;
|
||||
logger.e("pagingController", e, stack);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
|
||||
return FutureBuilder<CursorPage<Artist>>(
|
||||
future: spotifyApi.me.following(FollowingType.artist).first(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
pagingController.appendPage(items, items.last.id);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return PagedGridView(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
@ -69,14 +38,12 @@ class _UserArtistsState extends ConsumerState<UserArtists> {
|
||||
mainAxisSpacing: 20,
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
pagingController: _pagingController,
|
||||
pagingController: pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Artist>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return ArtistCard(item);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -27,10 +27,10 @@ class UserLibrary extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
body: auth.isLoggedIn
|
||||
? const TabBarView(children: [
|
||||
UserPlaylists(),
|
||||
? TabBarView(children: [
|
||||
const UserPlaylists(),
|
||||
UserArtists(),
|
||||
UserAlbums(),
|
||||
const UserAlbums(),
|
||||
])
|
||||
: const AnonymousFallback(),
|
||||
),
|
||||
|
48
lib/hooks/usePaginatedFutureProvider.dart
Normal file
48
lib/hooks/usePaginatedFutureProvider.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:spotube/hooks/usePagingController.dart';
|
||||
|
||||
PagingController<P, ItemType> usePaginatedFutureProvider<T, P, ItemType>(
|
||||
AutoDisposeFutureProvider<T> Function(P pageKey) createSnapshot, {
|
||||
required P firstPageKey,
|
||||
required WidgetRef ref,
|
||||
void Function(
|
||||
AsyncData<T>,
|
||||
PagingController<P, ItemType> pagingController,
|
||||
P pageKey,
|
||||
)?
|
||||
onData,
|
||||
void Function(AsyncError<T>)? onError,
|
||||
void Function(AsyncLoading<T>)? onLoading,
|
||||
}) {
|
||||
final currentPageKey = useState(firstPageKey);
|
||||
final snapshot = ref.watch(createSnapshot(currentPageKey.value));
|
||||
final pagingController =
|
||||
usePagingController<P, ItemType>(firstPageKey: firstPageKey);
|
||||
useEffect(() {
|
||||
listener(pageKey) {
|
||||
if (currentPageKey.value != pageKey) {
|
||||
currentPageKey.value = pageKey;
|
||||
}
|
||||
}
|
||||
|
||||
pagingController.addPageRequestListener(listener);
|
||||
return () => pagingController.removePageRequestListener(listener);
|
||||
}, [snapshot, currentPageKey]);
|
||||
|
||||
useEffect(() {
|
||||
snapshot.mapOrNull(
|
||||
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;
|
||||
}
|
@ -3,7 +3,7 @@ import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
|
||||
final categoriesQuery = FutureProvider.family<Page<Category>, int>(
|
||||
final categoriesQuery = FutureProvider.autoDispose.family<Page<Category>, int>(
|
||||
(ref, pageKey) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final recommendationMarket = ref.watch(
|
||||
@ -15,6 +15,20 @@ final categoriesQuery = FutureProvider.family<Page<Category>, int>(
|
||||
},
|
||||
);
|
||||
|
||||
final categoryPlaylistsQuery =
|
||||
FutureProvider.autoDispose.family<Page<PlaylistSimple>, String>(
|
||||
(ref, value) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final List data = value.split("/");
|
||||
final id = data.first;
|
||||
final pageKey = data.last;
|
||||
return (id != "user-featured-playlists"
|
||||
? spotify.playlists.getByCategoryId(id)
|
||||
: spotify.playlists.featured)
|
||||
.getPage(3, int.parse(pageKey));
|
||||
},
|
||||
);
|
||||
|
||||
final currentUserPlaylistsQuery = FutureProvider<Iterable<PlaylistSimple>>(
|
||||
(ref) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
@ -28,3 +42,11 @@ final currentUserAlbumsQuery = FutureProvider<Iterable<AlbumSimple>>(
|
||||
return spotify.me.savedAlbums().all();
|
||||
},
|
||||
);
|
||||
|
||||
final currentUserFollowingArtistsQuery =
|
||||
FutureProvider.autoDispose.family<CursorPage<Artist>, String>(
|
||||
(ref, pageKey) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
return spotify.me.following(FollowingType.artist).getPage(15, pageKey);
|
||||
},
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user