Merge branch 'master' into build

This commit is contained in:
Kingkor Roy Tirtho 2022-05-29 23:33:32 +06:00
commit a28d431817
7 changed files with 497 additions and 483 deletions

View File

@ -18,6 +18,7 @@ import 'package:spotube/hooks/useForceUpdate.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
class ArtistProfile extends HookConsumerWidget { class ArtistProfile extends HookConsumerWidget {
final String artistId; final String artistId;
@ -49,296 +50,283 @@ class ArtistProfile extends HookConsumerWidget {
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final update = useForceUpdate(); final update = useForceUpdate();
final Playback playback = ref.watch(playbackProvider);
final artistsSnapshot = ref.watch(artistProfileQuery(artistId));
final isFollowingSnapshot =
ref.watch(currentUserFollowsArtistQuery(artistId));
final topTracksSnapshot = ref.watch(artistTopTracksQuery(artistId));
final albums = ref.watch(artistAlbumsQuery(artistId));
final relatedArtists = ref.watch(artistRelatedArtistsQuery(artistId));
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
appBar: const PageWindowTitleBar( appBar: const PageWindowTitleBar(
leading: BackButton(), leading: BackButton(),
), ),
body: FutureBuilder<Artist>( body: artistsSnapshot.when<Widget>(
future: spotify.artists.get(artistId), data: (data) {
builder: (context, snapshot) { return SingleChildScrollView(
if (!snapshot.hasData) { controller: parentScrollController,
return const Center(child: CircularProgressIndicator.adaptive()); padding: const EdgeInsets.all(20),
} child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
return SingleChildScrollView( children: [
controller: parentScrollController, Wrap(
padding: const EdgeInsets.all(20), crossAxisAlignment: WrapCrossAlignment.center,
child: Column( runAlignment: WrapAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ const SizedBox(width: 50),
Wrap( CircleAvatar(
crossAxisAlignment: WrapCrossAlignment.center, radius: avatarWidth,
runAlignment: WrapAlignment.center, backgroundImage: CachedNetworkImageProvider(
children: [ imageToUrlString(data.images),
const SizedBox(width: 50),
CircleAvatar(
radius: avatarWidth,
backgroundImage: CachedNetworkImageProvider(
imageToUrlString(snapshot.data!.images),
),
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50)),
child: Text(snapshot.data!.type!.toUpperCase(),
style: chipTextVariant?.copyWith(
color: Colors.white)),
),
Text(
snapshot.data!.name!,
style: breakpoint.isSm
? textTheme.headline4
: textTheme.headline2,
),
Text(
"${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers",
style: breakpoint.isSm
? textTheme.bodyText1
: textTheme.headline5,
),
const SizedBox(height: 20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FutureBuilder<List<bool>>(
future: spotify.me.isFollowing(
FollowingType.artist,
[artistId],
),
builder: (context, snapshot) {
final isFollowing =
snapshot.data?.first == true;
return OutlinedButton(
onPressed: () async {
try {
isFollowing
? await spotify.me.unfollow(
FollowingType.artist,
[artistId],
)
: await spotify.me.follow(
FollowingType.artist,
[artistId],
);
} catch (e, stack) {
logger.e(
"FollowButton.onPressed",
e,
stack,
);
} finally {
update();
}
},
child: snapshot.hasData
? Text(isFollowing
? "Following"
: "Follow")
: const CircularProgressIndicator
.adaptive(),
);
}),
IconButton(
icon: const Icon(Icons.share_rounded),
onPressed: () {
Clipboard.setData(
ClipboardData(
text: snapshot
.data?.externalUrls?.spotify),
).then((val) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
"Artist URL copied to clipboard",
textAlign: TextAlign.center,
),
),
);
});
},
)
],
)
],
),
),
],
),
const SizedBox(height: 50),
FutureBuilder<Iterable<Track>>(
future:
spotify.artists.getTopTracks(snapshot.data!.id!, "US"),
builder: (context, trackSnapshot) {
if (!trackSnapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive());
}
Playback playback = ref.watch(playbackProvider);
var isPlaylistPlaying =
playback.currentPlaylist?.id == snapshot.data?.id;
playPlaylist(List<Track> tracks,
{Track? currentTrack}) async {
currentTrack ??= tracks.first;
if (!isPlaylistPlaying) {
playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks,
id: snapshot.data!.id!,
name: "${snapshot.data!.name!} To Tracks",
thumbnail: imageToUrlString(snapshot.data?.images),
);
playback.setCurrentTrack = currentTrack;
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playback.currentTrack?.id) {
playback.setCurrentTrack = currentTrack;
}
await playback.startPlaying();
}
return Column(children: [
Row(
children: [
Text(
"Top Tracks",
style: Theme.of(context).textTheme.headline4,
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 5),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(50),
),
child: IconButton(
icon: Icon(isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded),
color: Colors.white,
onPressed: trackSnapshot.hasData
? () => playPlaylist(
trackSnapshot.data!.toList())
: null,
),
)
],
),
...trackSnapshot.data
?.toList()
.asMap()
.entries
.map((track) {
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
String? thumbnailUrl = imageToUrlString(
track.value.album?.images,
index:
(track.value.album?.images?.length ?? 1) -
1);
return TrackTile(
playback,
duration: duration,
track: track,
thumbnailUrl: thumbnailUrl,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
trackSnapshot.data!.toList(),
currentTrack: track.value,
),
);
}) ??
[],
]);
},
),
const SizedBox(height: 50),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Albums",
style: Theme.of(context).textTheme.headline4,
),
TextButton(
child: const Text("See All"),
onPressed: () {
GoRouter.of(context).push(
"/artist-album/$artistId",
extra: snapshot.data?.name ?? "KRTX",
);
},
)
],
),
const SizedBox(height: 10),
FutureBuilder<List<Album>>(
future: spotify.artists
.albums(snapshot.data!.id!)
.getPage(5, 0)
.then((al) => al.items?.toList() ?? []),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive());
}
return Scrollbar(
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: snapshot.data
?.map((album) => AlbumCard(album))
.toList() ??
[],
), ),
), ),
); Padding(
}, padding: const EdgeInsets.all(20),
), child: Column(
const SizedBox(height: 20), mainAxisSize: MainAxisSize.min,
Text( crossAxisAlignment: CrossAxisAlignment.start,
"Fans also likes", children: [
style: Theme.of(context).textTheme.headline4, Container(
), padding: const EdgeInsets.symmetric(
const SizedBox(height: 10), horizontal: 10, vertical: 5),
FutureBuilder<Iterable<Artist>>( decoration: BoxDecoration(
future: spotify.artists.getRelatedArtists(artistId), color: Colors.blue,
builder: (context, snapshot) { borderRadius: BorderRadius.circular(50)),
if (!snapshot.hasData) { child: Text(data.type!.toUpperCase(),
return const Center( style: chipTextVariant?.copyWith(
child: CircularProgressIndicator.adaptive()); color: Colors.white)),
} ),
Text(
return Center( data.name!,
child: Wrap( style: breakpoint.isSm
spacing: 20, ? textTheme.headline4
runSpacing: 20, : textTheme.headline2,
children: snapshot.data ),
?.map((artist) => ArtistCard(artist)) Text(
.toList() ?? "${toReadableNumber(data.followers!.total!.toDouble())} followers",
[], style: breakpoint.isSm
? textTheme.bodyText1
: textTheme.headline5,
),
const SizedBox(height: 20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
isFollowingSnapshot.when(
data: (isFollowing) {
return OutlinedButton(
onPressed: () async {
try {
isFollowing
? await spotify.me.unfollow(
FollowingType.artist,
[artistId],
)
: await spotify.me.follow(
FollowingType.artist,
[artistId],
);
} catch (e, stack) {
logger.e(
"FollowButton.onPressed",
e,
stack,
);
} finally {
ref.refresh(
currentUserFollowsArtistQuery(
artistId),
);
}
},
child: Text(
isFollowing
? "Following"
: "Follow",
),
);
},
error: (error, stackTrace) => Container(),
loading: () =>
const CircularProgressIndicator
.adaptive()),
IconButton(
icon: const Icon(Icons.share_rounded),
onPressed: () {
Clipboard.setData(
ClipboardData(
text: data.externalUrls?.spotify),
).then((val) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
"Artist URL copied to clipboard",
textAlign: TextAlign.center,
),
),
);
});
},
)
],
)
],
),
), ),
); ],
}, ),
) const SizedBox(height: 50),
], topTracksSnapshot.when(
), data: (topTracks) {
); final isPlaylistPlaying =
}, playback.currentPlaylist?.id == data.id;
), playPlaylist(List<Track> tracks,
{Track? currentTrack}) async {
currentTrack ??= tracks.first;
if (!isPlaylistPlaying) {
playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks,
id: data.id!,
name: "${data.name!} To Tracks",
thumbnail: imageToUrlString(data.images),
);
playback.setCurrentTrack = currentTrack;
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playback.currentTrack?.id) {
playback.setCurrentTrack = currentTrack;
}
await playback.startPlaying();
}
return Column(children: [
Row(
children: [
Text(
"Top Tracks",
style: Theme.of(context).textTheme.headline4,
),
Container(
margin:
const EdgeInsets.symmetric(horizontal: 5),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(50),
),
child: IconButton(
icon: Icon(isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded),
color: Colors.white,
onPressed: () =>
playPlaylist(topTracks.toList()),
),
)
],
),
...topTracks.toList().asMap().entries.map((track) {
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
String? thumbnailUrl = imageToUrlString(
track.value.album?.images,
index:
(track.value.album?.images?.length ?? 1) -
1);
return TrackTile(
playback,
duration: duration,
track: track,
thumbnailUrl: thumbnailUrl,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
topTracks.toList(),
currentTrack: track.value,
),
);
}),
]);
},
error: (error, stack) =>
Text("Failed to find top tracks $error"),
loading: () => const Center(
child: CircularProgressIndicator.adaptive()),
),
const SizedBox(height: 50),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Albums",
style: Theme.of(context).textTheme.headline4,
),
TextButton(
child: const Text("See All"),
onPressed: () {
GoRouter.of(context).push(
"/artist-album/$artistId",
extra: data.name ?? "KRTX",
);
},
)
],
),
const SizedBox(height: 10),
albums.when(
data: (albums) {
return Scrollbar(
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: albums.items
?.map((album) => AlbumCard(album))
.toList() ??
[],
),
),
);
},
error: (error, stackTrack) =>
Text("Failed to get Artist albums $error"),
loading: () => const CircularProgressIndicator.adaptive(),
),
const SizedBox(height: 20),
Text(
"Fans also likes",
style: Theme.of(context).textTheme.headline4,
),
const SizedBox(height: 10),
relatedArtists.when(
data: (artists) {
return Center(
child: Wrap(
spacing: 20,
runSpacing: 20,
children: artists
.map((artist) => ArtistCard(artist))
.toList(),
),
);
},
error: (error, stackTrack) =>
Text("Failed to get Artist albums $error"),
loading: () => const CircularProgressIndicator.adaptive(),
),
],
),
);
},
error: (_, __) => const Text("Life's miserable"),
loading: () =>
const Center(child: CircularProgressIndicator.adaptive())),
), ),
); );
} }

View File

@ -4,11 +4,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Playlist/PlaylistCard.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/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 Category category;
final Iterable<PlaylistSimple>? playlists; final Iterable<PlaylistSimple>? playlists;
CategoryCard( CategoryCard(
@ -20,7 +20,32 @@ class CategoryCard extends HookWidget {
final logger = getLogger(CategoryCard); final logger = getLogger(CategoryCard);
@override @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: (page, pagingController, pageKey) {
if (playlists != null && playlists?.isNotEmpty == true && mounted()) {
return pagingController.appendLastPage(playlists!.toList());
}
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( return Column(
children: [ children: [
Padding( Padding(
@ -34,73 +59,25 @@ class CategoryCard extends HookWidget {
], ],
), ),
), ),
HookConsumer( pagingController.error != null
builder: (context, ref, child) { ? const Text("Something Went Wrong")
SpotifyApi spotifyApi = ref.watch(spotifyProvider); : SizedBox(
final scrollController = useScrollController(); height: 245,
final pagingController = child: Scrollbar(
usePagingController<int, PlaylistSimple>(firstPageKey: 0); controller: scrollController,
child: PagedListView<int, PlaylistSimple>(
final _error = useState(false); shrinkWrap: true,
final mounted = useIsMounted(); pagingController: pagingController,
scrollController: scrollController,
useEffect(() { scrollDirection: Axis.horizontal,
listener(pageKey) async { builderDelegate: PagedChildBuilderDelegate<PlaylistSimple>(
try { itemBuilder: (context, playlist, index) {
if (playlists != null && return PlaylistCard(playlist);
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(
height: 245,
child: Scrollbar(
controller: scrollController,
child: PagedListView<int, PlaylistSimple>(
shrinkWrap: true,
pagingController: pagingController,
scrollController: scrollController,
scrollDirection: Axis.horizontal,
builderDelegate: PagedChildBuilderDelegate<PlaylistSimple>(
itemBuilder: (context, playlist, index) {
return PlaylistCard(playlist);
},
), ),
), ),
), )
);
},
)
], ],
); );
} }

View File

@ -18,11 +18,9 @@ import 'package:spotube/components/Player/Player.dart';
import 'package:spotube/components/Library/UserLibrary.dart'; import 'package:spotube/components/Library/UserLibrary.dart';
import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useHotKeys.dart'; import 'package:spotube/hooks/useHotKeys.dart';
import 'package:spotube/hooks/usePagingController.dart'; import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
import 'package:spotube/hooks/useSharedPreferences.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
import 'package:spotube/provider/UserPreferences.dart';
List<String> spotifyScopes = [ List<String> spotifyScopes = [
"playlist-modify-public", "playlist-modify-public",
@ -42,12 +40,6 @@ class Home extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { 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( final int titleBarDragMaxWidth = useBreakpointValue(
md: 72, md: 72,
lg: 256, lg: 256,
@ -58,53 +50,9 @@ class Home extends HookConsumerWidget {
final _selectedIndex = useState(0); final _selectedIndex = useState(0);
_onSelectedIndexChanged(int index) => _selectedIndex.value = index; _onSelectedIndexChanged(int index) => _selectedIndex.value = index;
final localStorage = useSharedPreferences();
// initializing global hot keys // initializing global hot keys
useHotKeys(ref); 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( final titleBarContents = Container(
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
child: Row( child: Row(
@ -160,14 +108,38 @@ class Home extends HookConsumerWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: PagedListView( child: HookBuilder(builder: (context) {
pagingController: pagingController, final pagingController = usePaginatedFutureProvider<
builderDelegate: PagedChildBuilderDelegate<Category>( Page<Category>, int, Category>(
itemBuilder: (context, item, index) { (pageKey) => categoriesQuery(pageKey),
return CategoryCard(item); ref: ref,
firstPageKey: 0,
onData: (categories, pagingController, 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 && items != null) {
pagingController.appendLastPage(items);
} else if (categories.items != null) {
pagingController.appendPage(
items!, categories.nextOffset);
}
}, },
), );
), return PagedListView(
pagingController: pagingController,
builderDelegate:
PagedChildBuilderDelegate<Category>(
itemBuilder: (context, item, index) {
return CategoryCard(item);
},
),
);
}),
), ),
), ),
if (_selectedIndex.value == 1) const Search(), if (_selectedIndex.value == 1) const Search(),

View File

@ -1,82 +1,48 @@
import 'package:flutter/material.dart'; 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:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Artist/ArtistCard.dart'; import 'package:spotube/components/Artist/ArtistCard.dart';
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyRequests.dart';
class UserArtists extends ConsumerStatefulWidget { class UserArtists extends HookConsumerWidget {
const UserArtists({Key? key}) : super(key: key); UserArtists({Key? key}) : super(key: key);
@override
ConsumerState<UserArtists> createState() => _UserArtistsState();
}
class _UserArtistsState extends ConsumerState<UserArtists> {
final PagingController<String, Artist> _pagingController =
PagingController(firstPageKey: "");
final logger = getLogger(UserArtists); final logger = getLogger(UserArtists);
@override @override
void initState() { Widget build(BuildContext context, ref) {
super.initState(); final pagingController =
WidgetsBinding.instance.addPostFrameCallback((timestamp) { usePaginatedFutureProvider<CursorPage<Artist>, String, Artist>(
_pagingController.addPageRequestListener((pageKey) async { (pageKey) => currentUserFollowingArtistsQuery(pageKey),
try { ref: ref,
SpotifyApi spotifyApi = ref.read(spotifyProvider); firstPageKey: "",
CursorPage<Artist> artists = await spotifyApi.me onData: (artists, pagingController, pageKey) {
.following(FollowingType.artist) final items = artists.items!.toList();
.getPage(15, pageKey);
var items = artists.items!.toList(); if (artists.items != null && items.length < 15) {
pagingController.appendLastPage(items);
if (artists.items != null && items.length < 15) { } else if (artists.items != null) {
_pagingController.appendLastPage(items); pagingController.appendPage(items, items.last.id);
} 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());
}
return PagedGridView(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250,
childAspectRatio: 9 / 11,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
padding: const EdgeInsets.all(10),
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Artist>(
itemBuilder: (context, item, index) {
return ArtistCard(item);
},
),
);
}, },
); );
return PagedGridView(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250,
childAspectRatio: 9 / 11,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
padding: const EdgeInsets.all(10),
pagingController: pagingController,
builderDelegate: PagedChildBuilderDelegate<Artist>(
itemBuilder: (context, item, index) {
return ArtistCard(item);
},
),
);
} }
} }

View File

@ -27,10 +27,10 @@ class UserLibrary extends ConsumerWidget {
], ],
), ),
body: auth.isLoggedIn body: auth.isLoggedIn
? const TabBarView(children: [ ? TabBarView(children: [
UserPlaylists(), const UserPlaylists(),
UserArtists(), UserArtists(),
UserAlbums(), const UserAlbums(),
]) ])
: const AnonymousFallback(), : const AnonymousFallback(),
), ),

View 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(
T,
PagingController<P, ItemType> pagingController,
P pageKey,
)?
onData,
void Function(Object)? onError,
void Function()? 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.whenOrNull(
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;
}

View File

@ -3,7 +3,7 @@ import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
final categoriesQuery = FutureProvider.family<Page<Category>, int>( final categoriesQuery = FutureProvider.autoDispose.family<Page<Category>, int>(
(ref, pageKey) { (ref, pageKey) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final recommendationMarket = ref.watch( 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>>( final currentUserPlaylistsQuery = FutureProvider<Iterable<PlaylistSimple>>(
(ref) { (ref) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
@ -28,3 +42,52 @@ final currentUserAlbumsQuery = FutureProvider<Iterable<AlbumSimple>>(
return spotify.me.savedAlbums().all(); 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);
},
);
final artistProfileQuery = FutureProvider.autoDispose.family<Artist, String>(
(ref, id) {
final spotify = ref.watch(spotifyProvider);
return spotify.artists.get(id);
},
);
final currentUserFollowsArtistQuery =
FutureProvider.autoDispose.family<bool, String>(
(ref, artistId) async {
final spotify = ref.watch(spotifyProvider);
final result = await spotify.me.isFollowing(
FollowingType.artist,
[artistId],
);
return result.first;
},
);
final artistTopTracksQuery =
FutureProvider.autoDispose.family<Iterable<Track>, String>((ref, id) {
final spotify = ref.watch(spotifyProvider);
return spotify.artists.getTopTracks(id, "US");
});
final artistAlbumsQuery =
FutureProvider.autoDispose.family<Page<Album>, String>(
(ref, id) {
final spotify = ref.watch(spotifyProvider);
return spotify.artists.albums(id).getPage(5, 0);
},
);
final artistRelatedArtistsQuery =
FutureProvider.autoDispose.family<Iterable<Artist>, String>(
(ref, id) {
final spotify = ref.watch(spotifyProvider);
return spotify.artists.getRelatedArtists(id);
},
);