mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
Merge branch 'master' into build
This commit is contained in:
commit
a28d431817
@ -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,18 +50,22 @@ 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) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const Center(child: CircularProgressIndicator.adaptive());
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
controller: parentScrollController,
|
controller: parentScrollController,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
@ -75,7 +80,7 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: avatarWidth,
|
radius: avatarWidth,
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
backgroundImage: CachedNetworkImageProvider(
|
||||||
imageToUrlString(snapshot.data!.images),
|
imageToUrlString(data.images),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@ -90,18 +95,18 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
borderRadius: BorderRadius.circular(50)),
|
borderRadius: BorderRadius.circular(50)),
|
||||||
child: Text(snapshot.data!.type!.toUpperCase(),
|
child: Text(data.type!.toUpperCase(),
|
||||||
style: chipTextVariant?.copyWith(
|
style: chipTextVariant?.copyWith(
|
||||||
color: Colors.white)),
|
color: Colors.white)),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
snapshot.data!.name!,
|
data.name!,
|
||||||
style: breakpoint.isSm
|
style: breakpoint.isSm
|
||||||
? textTheme.headline4
|
? textTheme.headline4
|
||||||
: textTheme.headline2,
|
: textTheme.headline2,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers",
|
"${toReadableNumber(data.followers!.total!.toDouble())} followers",
|
||||||
style: breakpoint.isSm
|
style: breakpoint.isSm
|
||||||
? textTheme.bodyText1
|
? textTheme.bodyText1
|
||||||
: textTheme.headline5,
|
: textTheme.headline5,
|
||||||
@ -110,14 +115,8 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
FutureBuilder<List<bool>>(
|
isFollowingSnapshot.when(
|
||||||
future: spotify.me.isFollowing(
|
data: (isFollowing) {
|
||||||
FollowingType.artist,
|
|
||||||
[artistId],
|
|
||||||
),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final isFollowing =
|
|
||||||
snapshot.data?.first == true;
|
|
||||||
return OutlinedButton(
|
return OutlinedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
try {
|
||||||
@ -137,24 +136,29 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
stack,
|
stack,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
update();
|
ref.refresh(
|
||||||
|
currentUserFollowsArtistQuery(
|
||||||
|
artistId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: snapshot.hasData
|
child: Text(
|
||||||
? Text(isFollowing
|
isFollowing
|
||||||
? "Following"
|
? "Following"
|
||||||
: "Follow")
|
: "Follow",
|
||||||
: const CircularProgressIndicator
|
),
|
||||||
.adaptive(),
|
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
|
error: (error, stackTrace) => Container(),
|
||||||
|
loading: () =>
|
||||||
|
const CircularProgressIndicator
|
||||||
|
.adaptive()),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.share_rounded),
|
icon: const Icon(Icons.share_rounded),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(
|
ClipboardData(
|
||||||
text: snapshot
|
text: data.externalUrls?.spotify),
|
||||||
.data?.externalUrls?.spotify),
|
|
||||||
).then((val) {
|
).then((val) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
.showSnackBar(
|
.showSnackBar(
|
||||||
@ -178,26 +182,19 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 50),
|
const SizedBox(height: 50),
|
||||||
FutureBuilder<Iterable<Track>>(
|
topTracksSnapshot.when(
|
||||||
future:
|
data: (topTracks) {
|
||||||
spotify.artists.getTopTracks(snapshot.data!.id!, "US"),
|
final isPlaylistPlaying =
|
||||||
builder: (context, trackSnapshot) {
|
playback.currentPlaylist?.id == data.id;
|
||||||
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,
|
playPlaylist(List<Track> tracks,
|
||||||
{Track? currentTrack}) async {
|
{Track? currentTrack}) async {
|
||||||
currentTrack ??= tracks.first;
|
currentTrack ??= tracks.first;
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
playback.setCurrentPlaylist = CurrentPlaylist(
|
playback.setCurrentPlaylist = CurrentPlaylist(
|
||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
id: snapshot.data!.id!,
|
id: data.id!,
|
||||||
name: "${snapshot.data!.name!} To Tracks",
|
name: "${data.name!} To Tracks",
|
||||||
thumbnail: imageToUrlString(snapshot.data?.images),
|
thumbnail: imageToUrlString(data.images),
|
||||||
);
|
);
|
||||||
playback.setCurrentTrack = currentTrack;
|
playback.setCurrentTrack = currentTrack;
|
||||||
} else if (isPlaylistPlaying &&
|
} else if (isPlaylistPlaying &&
|
||||||
@ -216,7 +213,8 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
style: Theme.of(context).textTheme.headline4,
|
style: Theme.of(context).textTheme.headline4,
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 5),
|
margin:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
borderRadius: BorderRadius.circular(50),
|
borderRadius: BorderRadius.circular(50),
|
||||||
@ -226,19 +224,13 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
? Icons.stop_rounded
|
? Icons.stop_rounded
|
||||||
: Icons.play_arrow_rounded),
|
: Icons.play_arrow_rounded),
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: trackSnapshot.hasData
|
onPressed: () =>
|
||||||
? () => playPlaylist(
|
playPlaylist(topTracks.toList()),
|
||||||
trackSnapshot.data!.toList())
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
...trackSnapshot.data
|
...topTracks.toList().asMap().entries.map((track) {
|
||||||
?.toList()
|
|
||||||
.asMap()
|
|
||||||
.entries
|
|
||||||
.map((track) {
|
|
||||||
String duration =
|
String duration =
|
||||||
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||||
String? thumbnailUrl = imageToUrlString(
|
String? thumbnailUrl = imageToUrlString(
|
||||||
@ -253,14 +245,17 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
thumbnailUrl: thumbnailUrl,
|
thumbnailUrl: thumbnailUrl,
|
||||||
onTrackPlayButtonPressed: (currentTrack) =>
|
onTrackPlayButtonPressed: (currentTrack) =>
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
trackSnapshot.data!.toList(),
|
topTracks.toList(),
|
||||||
currentTrack: track.value,
|
currentTrack: track.value,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}) ??
|
}),
|
||||||
[],
|
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
error: (error, stack) =>
|
||||||
|
Text("Failed to find top tracks $error"),
|
||||||
|
loading: () => const Center(
|
||||||
|
child: CircularProgressIndicator.adaptive()),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 50),
|
const SizedBox(height: 50),
|
||||||
Row(
|
Row(
|
||||||
@ -275,23 +270,15 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).push(
|
GoRouter.of(context).push(
|
||||||
"/artist-album/$artistId",
|
"/artist-album/$artistId",
|
||||||
extra: snapshot.data?.name ?? "KRTX",
|
extra: data.name ?? "KRTX",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
FutureBuilder<List<Album>>(
|
albums.when(
|
||||||
future: spotify.artists
|
data: (albums) {
|
||||||
.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(
|
return Scrollbar(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@ -299,7 +286,7 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: snapshot.data
|
children: albums.items
|
||||||
?.map((album) => AlbumCard(album))
|
?.map((album) => AlbumCard(album))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[],
|
||||||
@ -307,6 +294,9 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
error: (error, stackTrack) =>
|
||||||
|
Text("Failed to get Artist albums $error"),
|
||||||
|
loading: () => const CircularProgressIndicator.adaptive(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
@ -314,31 +304,29 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
style: Theme.of(context).textTheme.headline4,
|
style: Theme.of(context).textTheme.headline4,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
FutureBuilder<Iterable<Artist>>(
|
relatedArtists.when(
|
||||||
future: spotify.artists.getRelatedArtists(artistId),
|
data: (artists) {
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator.adaptive());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 20,
|
spacing: 20,
|
||||||
runSpacing: 20,
|
runSpacing: 20,
|
||||||
children: snapshot.data
|
children: artists
|
||||||
?.map((artist) => ArtistCard(artist))
|
.map((artist) => ArtistCard(artist))
|
||||||
.toList() ??
|
.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())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,55 +59,9 @@ 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();
|
|
||||||
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(
|
|
||||||
height: 245,
|
height: 245,
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
@ -98,8 +77,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/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) {
|
||||||
|
final pagingController = usePaginatedFutureProvider<
|
||||||
|
Page<Category>, int, Category>(
|
||||||
|
(pageKey) => categoriesQuery(pageKey),
|
||||||
|
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,
|
pagingController: pagingController,
|
||||||
builderDelegate: PagedChildBuilderDelegate<Category>(
|
builderDelegate:
|
||||||
|
PagedChildBuilderDelegate<Category>(
|
||||||
itemBuilder: (context, item, index) {
|
itemBuilder: (context, item, index) {
|
||||||
return CategoryCard(item);
|
return CategoryCard(item);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_selectedIndex.value == 1) const Search(),
|
if (_selectedIndex.value == 1) const Search(),
|
||||||
|
@ -1,65 +1,33 @@
|
|||||||
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) {
|
if (artists.items != null && items.length < 15) {
|
||||||
_pagingController.appendLastPage(items);
|
pagingController.appendLastPage(items);
|
||||||
} else if (artists.items != null) {
|
} else if (artists.items != null) {
|
||||||
_pagingController.appendPage(items, items.last.id);
|
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(
|
return PagedGridView(
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
@ -69,14 +37,12 @@ class _UserArtistsState extends ConsumerState<UserArtists> {
|
|||||||
mainAxisSpacing: 20,
|
mainAxisSpacing: 20,
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
pagingController: _pagingController,
|
pagingController: pagingController,
|
||||||
builderDelegate: PagedChildBuilderDelegate<Artist>(
|
builderDelegate: PagedChildBuilderDelegate<Artist>(
|
||||||
itemBuilder: (context, item, index) {
|
itemBuilder: (context, item, index) {
|
||||||
return ArtistCard(item);
|
return ArtistCard(item);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
),
|
),
|
||||||
|
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(
|
||||||
|
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;
|
||||||
|
}
|
@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user