diff --git a/src/components/Library.tsx b/src/components/Library.tsx index cce426fd..1e5acc41 100644 --- a/src/components/Library.tsx +++ b/src/components/Library.tsx @@ -5,10 +5,11 @@ import { QueryCacheKeys } from "../conf"; import playerContext from "../context/playerContext"; import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery"; import useSpotifyQuery from "../hooks/useSpotifyQuery"; +import { GenreView } from "./PlaylistGenreView"; import { PlaylistSimpleControls, TrackTableIndex } from "./PlaylistView"; import PlaceholderApplet from "./shared/PlaceholderApplet"; import PlaylistCard from "./shared/PlaylistCard"; -import { TrackButton } from "./shared/TrackButton"; +import { TrackButton, TrackButtonPlaylistObject } from "./shared/TrackButton"; import { TabMenuItem } from "./TabMenu"; function Library() { @@ -32,21 +33,36 @@ function Library() { export default Library; function UserPlaylists() { - const { data: userPlaylists, isError, isLoading, refetch } = useSpotifyQuery(QueryCacheKeys.userPlaylists, (spotifyApi) => - spotifyApi.getUserPlaylists().then((userPlaylists) => { - return userPlaylists.body.items; - }) + const { data: userPagedPlaylists, isError, isLoading, refetch, isFetchingNextPage, hasNextPage, fetchNextPage } = useSpotifyInfiniteQuery( + QueryCacheKeys.userPlaylists, + (spotifyApi, { pageParam }) => + spotifyApi.getUserPlaylists({ limit: 20, offset: pageParam }).then((userPlaylists) => { + return userPlaylists.body; + }), + { + getNextPageParam(lastPage) { + if (lastPage.next) { + return lastPage.offset + lastPage.limit; + } + }, + } ); + const userPlaylists = userPagedPlaylists?.pages + ?.map((playlist) => playlist.items) + .filter(Boolean) + .flat(1) as SpotifyApi.PlaylistObjectSimplified[]; + return ( - - - - {userPlaylists?.map((playlist, index) => ( - - ))} - - + fetchNextPage() : undefined} + /> ); } @@ -80,19 +96,11 @@ function UserSavedTracks() { } } - const playlist: SpotifyApi.PlaylistObjectFull = { + const playlist: TrackButtonPlaylistObject = { collaborative: false, description: "User Playlist", tracks: { - items: [userTracks ?? []].map( - (userTrack) => - (({ - ...userTrack, - added_by: "Me", - is_local: false, - added_at: Date.now(), - } as unknown) as SpotifyApi.PlaylistTrackObject) - ), + items: userTracks ?? [], limit: 20, href: "", next: "", @@ -101,10 +109,9 @@ function UserSavedTracks() { total: 20, }, external_urls: { spotify: "" }, - followers: { href: null, total: 2 }, href: "", id: userSavedPlaylistId, - images: [], + images: [{ url: "https://facebook.com/img.jpeg" }], name: "User saved track", owner: { external_urls: { spotify: "" }, href: "", id: "Me", type: "user", uri: "spotify:user:me", display_name: "User", followers: { href: null, total: 0 } }, public: false, diff --git a/src/components/shared/TrackButton.tsx b/src/components/shared/TrackButton.tsx index 679035c9..3cacdb25 100644 --- a/src/components/shared/TrackButton.tsx +++ b/src/components/shared/TrackButton.tsx @@ -5,26 +5,27 @@ import playerContext from "../../context/playerContext"; import { msToMinAndSec } from "../../helpers/msToMin:sec"; import useTrackReaction from "../../hooks/useTrackReaction"; import { heart, heartRegular, pause, play } from "../../icons"; -import { audioPlayer } from "../Player"; import IconButton from "./IconButton"; +export interface TrackButtonPlaylistObject extends SpotifyApi.PlaylistBaseObject { + follower?: SpotifyApi.FollowersObject; + tracks: SpotifyApi.PagingObject; +} + export interface TrackButtonProps { track: SpotifyApi.TrackObjectFull; - playlist?: SpotifyApi.PlaylistObjectFull; + playlist?: TrackButtonPlaylistObject; index: number; } export const TrackButton: FC = ({ track, index, playlist }) => { const { reactToTrack, isFavorite } = useTrackReaction(); const { currentPlaylist, setCurrentPlaylist, setCurrentTrack, currentTrack } = useContext(playerContext); - const handlePlaylistPlayPause = (index?: number) => { - if (currentPlaylist?.id !== playlist?.id && playlist?.tracks) { - setCurrentPlaylist({ id: playlist.id, name: playlist.name, thumbnail: playlist.images[0].url, tracks: playlist.tracks.items }); - setCurrentTrack(playlist.tracks.items[index ?? 0].track); - } else { - audioPlayer.stop().catch((error) => console.error("Failed to stop audio player: ", error)); - setCurrentTrack(undefined); - setCurrentPlaylist(undefined); + const handlePlaylistPlayPause = (index: number) => { + if (playlist && currentPlaylist?.id !== playlist.id) { + const globalPlaylistObj = { id: playlist.id, name: playlist.name, thumbnail: playlist.images[0].url, tracks: playlist.tracks.items }; + setCurrentPlaylist(globalPlaylistObj); + setCurrentTrack(playlist.tracks.items[index].track); } }; @@ -36,7 +37,7 @@ export const TrackButton: FC = ({ track, index, playlist }) => const active = (currentPlaylist?.id === playlist?.id && currentTrack?.id === track.id) || currentTrack?.id === track.id; return ( (QueryCacheKeys.userPlaylists, (spotifyApi) => - spotifyApi.getUserPlaylists().then((userPlaylists) => userPlaylists.body.items) + const { data: favoritePagedPlaylists } = useSpotifyInfiniteQuery(QueryCacheKeys.userPlaylists, (spotifyApi, { pageParam }) => + spotifyApi.getUserPlaylists({ limit: 20, offset: pageParam }).then((userPlaylists) => { + return userPlaylists.body; + }) ); + const favoritePlaylists = favoritePagedPlaylists?.pages + .map((playlist) => playlist.items) + .filter(Boolean) + .flat(1) as SpotifyApi.PlaylistObjectSimplified[]; + + function updateFunction(playlist: SpotifyApi.PlaylistObjectSimplified, old?: InfiniteData): InfiniteData { + const obj: typeof old = { + pageParams: old?.pageParams ?? [], + pages: + old?.pages.map( + (oldPage, index): SpotifyApi.ListOfUsersPlaylistsResponse => { + const isPlaylistFavorite = isFavorite(playlist.id); + if (index === 0 && !isPlaylistFavorite) { + return { ...oldPage, items: [...oldPage.items, playlist] }; + } else if (isPlaylistFavorite) { + return { ...oldPage, items: oldPage.items.filter((oldPlaylist) => oldPlaylist.id !== playlist.id) }; + } + return oldPage; + } + ) ?? [], + }; + return obj; + } + const { mutate: reactToPlaylist } = useSpotifyMutation<{}, SpotifyApi.PlaylistObjectSimplified>( (spotifyApi, { id }) => spotifyApi[isFavorite(id) ? "unfollowPlaylist" : "followPlaylist"](id).then((res) => res.body), { onSuccess(_, playlist) { - queryClient.setQueryData( - QueryCacheKeys.userPlaylists, - isFavorite(playlist.id) ? (old) => (old ?? []).filter((oldPlaylist) => oldPlaylist.id !== playlist.id) : (old) => [...(old ?? []), playlist] - ); + queryClient.setQueryData>(QueryCacheKeys.userPlaylists, (old) => updateFunction(playlist, old)); }, } ); diff --git a/src/hooks/useSpotifyApiError.ts b/src/hooks/useSpotifyApiError.ts index 467f1388..3f66dad8 100644 --- a/src/hooks/useSpotifyApiError.ts +++ b/src/hooks/useSpotifyApiError.ts @@ -7,17 +7,23 @@ import showError from "../helpers/showError"; function useSpotifyApiError(spotifyApi: SpotifyWebApi) { const { setAccess_token, isLoggedIn } = useContext(authContext); return async (error: any | Error | TypeError) => { - if ((error.message === "Unauthorized" && error.status === 401 && isLoggedIn) || (error.body.error.message === "No token provided" && error.body.error.status===401)) { + const isUnauthorized = error.message === "Unauthorized"; + const status401 = error.status === 401; + const bodyStatus401 = error.body.error.status === 401; + const noToken = error.body.error.message === "No token provided"; + const expiredToken = error.body.error.message === "The access token expired"; + if ((isUnauthorized && isLoggedIn && status401) || ((noToken || expiredToken) && bodyStatus401)) { try { - console.log(chalk.bgYellow.blackBright("Refreshing Access token")) - const { body:{access_token: refreshedAccessToken}} = await spotifyApi.refreshAccessToken(); + console.log(chalk.bgYellow.blackBright("Refreshing Access token")); + const { + body: { access_token: refreshedAccessToken }, + } = await spotifyApi.refreshAccessToken(); setAccess_token(refreshedAccessToken); } catch (error) { - showError(error, "[Authorization Failure]: ") + showError(error, "[Authorization Failure]: "); } } }; } - -export default useSpotifyApiError; \ No newline at end of file +export default useSpotifyApiError; diff --git a/src/hooks/useTrackReaction.ts b/src/hooks/useTrackReaction.ts index 5f6e3e72..118f874f 100644 --- a/src/hooks/useTrackReaction.ts +++ b/src/hooks/useTrackReaction.ts @@ -1,8 +1,7 @@ -import { useQueryClient } from "react-query"; +import { InfiniteData, useQueryClient } from "react-query"; import { QueryCacheKeys } from "../conf"; import useSpotifyInfiniteQuery from "./useSpotifyInfiniteQuery"; import useSpotifyMutation from "./useSpotifyMutation"; -import useSpotifyQuery from "./useSpotifyQuery"; function useTrackReaction() { const queryClient = useQueryClient(); @@ -13,14 +12,31 @@ function useTrackReaction() { ?.map((page) => page.items) .filter(Boolean) .flat(1) as SpotifyApi.SavedTrackObject[] | undefined; + + function updateFunction(track: SpotifyApi.SavedTrackObject, old?: InfiniteData): InfiniteData { + const obj: typeof old = { + pageParams: old?.pageParams ?? [], + pages: + old?.pages.map( + (oldPage, index): SpotifyApi.UsersSavedTracksResponse => { + const isTrackFavorite = isFavorite(track.track.id); + if (index === 0 && !isTrackFavorite) { + return { ...oldPage, items: [...oldPage.items, track] }; + } else if (isTrackFavorite) { + return { ...oldPage, items: oldPage.items.filter((oldTrack) => oldTrack.track.id !== track.track.id) }; + } + return oldPage; + } + ) ?? [], + }; + return obj; + } + const { mutate: reactToTrack } = useSpotifyMutation<{}, SpotifyApi.SavedTrackObject>( (spotifyApi, { track }) => spotifyApi[isFavorite(track.id) ? "removeFromMySavedTracks" : "addToMySavedTracks"]([track.id]).then((res) => res.body), { onSuccess(_, track) { - queryClient.setQueryData( - QueryCacheKeys.userSavedTracks, - isFavorite(track.track.id) ? (old) => (old ?? []).filter((oldTrack) => oldTrack.track.id !== track.track.id) : (old) => [...(old ?? []), track] - ); + queryClient.setQueryData>(QueryCacheKeys.userSavedTracks, (old) => updateFunction(track, old)); }, } );