Tabbed Interface with support for current playback & user library added

This commit is contained in:
krtirtho 2021-03-06 12:54:16 +06:00
parent fe39ab0ffd
commit 247f1b563b
15 changed files with 7802 additions and 172 deletions

BIN
assets/rickroll.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

7501
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -74,13 +74,10 @@ function RootApp() {
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
const [credentials, setCredentials] = useState<Credentials>({ clientId: "", clientSecret: "" });
const [expires_in, setExpires_in] = useState<number>(0);
const [access_token, setAccess_token] = useState<string>("");
const [currentPlaylist, setCurrentPlaylist] = useState<CurrentPlaylist>();
const cachedCredentials = localStorage.getItem(CredentialKeys.credentials);
const setExpireTime = (expirationDuration: number) => setExpires_in(Date.now() + expirationDuration * 1000 /* 1s = 1000 ms */);
useEffect(() => {
setIsLoggedIn(!!cachedCredentials);
}, []);
@ -109,7 +106,6 @@ function RootApp() {
spotifyApi.setClientSecret(credentials.clientSecret);
const { body: authRes } = await spotifyApi.authorizationCodeGrant(req.query.code);
setAccess_token(authRes.access_token);
setExpireTime(authRes.expires_in);
localStorage.setItem(CredentialKeys.refresh_token, authRes.refresh_token);
return res.end();
} catch (error) {
@ -119,7 +115,11 @@ function RootApp() {
const server = app.listen(4304, () => {
console.log("Server is running");
open(spotifyApi.createAuthorizeURL(["user-library-read", "user-library-modify"], "xxxyyysssddd")).catch((e) => console.error("Opening IPC connection with browser failed: ", e));
spotifyApi.setClientId(credentials.clientId);
spotifyApi.setClientSecret(credentials.clientSecret);
open(spotifyApi.createAuthorizeURL(["user-library-read", "playlist-read-private", "user-library-modify"], "xxxyyysssddd")).catch((e) =>
console.error("Opening IPC connection with browser failed: ", e)
);
});
return () => {
server.close(() => console.log("Closed server"));
@ -136,10 +136,10 @@ function RootApp() {
return (
<Window ref={windowRef} on={windowEvents} windowState={WindowState.WindowMaximized} windowIcon={winIcon} windowTitle="Spotube" minSize={minSize}>
<MemoryRouter>
<authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, expires_in, setAccess_token, setExpires_in: setExpireTime, ...credentials }}>
<authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, setAccess_token, ...credentials }}>
<playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}>
<QueryClientProvider client={queryClient}>
<View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}>
<View style={`flex: 1; flex-direction: 'column'; align-items: 'stretch';`}>
<Routes />
{isLoggedIn && <Player />}
</View>

View File

@ -0,0 +1,35 @@
import { ScrollArea, Text, View } from "@nodegui/react-nodegui";
import React, { useContext } from "react";
import playerContext from "../context/playerContext";
import { TrackButton } from "./PlaylistView";
function CurrentPlaylist() {
const { currentPlaylist, currentTrack, setCurrentTrack } = useContext(playerContext);
if (!currentPlaylist && !currentTrack) {
return <Text style="flex: 1;">{`<center>There is nothing being played now</center>`}</Text>;
}
return (
<View style="flex: 1; flex-direction: 'column';">
<Text>{ `<center><h2>${currentPlaylist?.name}</h2></center>` }</Text>
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
<View style={`flex-direction:column; flex: 1;`}>
{currentPlaylist?.tracks.map(({ track }, index) => {
return (
<TrackButton
key={index + track.id}
active={currentTrack?.id === track.id}
artist={track.artists.map((x) => x.name).join(", ")}
name={track.name}
on={{ clicked: () => setCurrentTrack(track) }}
/>
);
})}
</View>
</ScrollArea>
</View>
);
}
export default CurrentPlaylist;

View File

@ -8,34 +8,18 @@ import useSpotifyQuery from "../hooks/useSpotifyQuery";
import ErrorApplet from "./shared/ErrorApplet";
function Home() {
const {
data: categories,
isError,
isRefetchError,
refetch,
} = useSpotifyQuery<SpotifyApi.CategoryObject[]>(
const { data: categories, isError, isRefetchError, refetch } = useSpotifyQuery<SpotifyApi.CategoryObject[]>(
QueryCacheKeys.categories,
(spotifyApi) =>
spotifyApi
.getCategories({ country: "US" })
.then((categoriesReceived) => categoriesReceived.body.categories.items),
(spotifyApi) => spotifyApi.getCategories({ country: "US" }).then((categoriesReceived) => categoriesReceived.body.categories.items),
{ initialData: [] }
);
return (
<ScrollArea style={`flex-grow: 1; border: none;`}>
<ScrollArea style={`flex-grow: 1; border: none; flex: 1;`}>
<View style={`flex-direction: 'column'; justify-content: 'center'; flex: 1;`}>
{(isError || isRefetchError) && (
<ErrorApplet message="Failed to query genres" reload={refetch} helps />
)}
{(isError || isRefetchError) && <ErrorApplet message="Failed to query genres" reload={refetch} helps />}
{categories?.map((category, index) => {
return (
<CategoryCard
key={index + category.id}
id={category.id}
name={category.name}
/>
);
return <CategoryCard key={index + category.id} id={category.id} name={category.name} />;
})}
</View>
</ScrollArea>
@ -51,14 +35,9 @@ interface CategoryCardProps {
const CategoryCard = ({ id, name }: CategoryCardProps) => {
const history = useHistory();
const { data: playlists, isError } = useSpotifyQuery<
SpotifyApi.PlaylistObjectSimplified[]
>(
const { data: playlists, isError } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(
[QueryCacheKeys.categoryPlaylists, id],
(spotifyApi) =>
spotifyApi
.getPlaylistsForCategory(id, { limit: 4 })
.then((playlistsRes) => playlistsRes.body.playlists.items),
(spotifyApi) => spotifyApi.getPlaylistsForCategory(id, { limit: 4 }).then((playlistsRes) => playlistsRes.body.playlists.items),
{ initialData: [] }
);
@ -66,26 +45,14 @@ const CategoryCard = ({ id, name }: CategoryCardProps) => {
history.push(`/genre/playlists/${id}`, { name });
}
if (isError) {
return <></ >;
return <></>;
}
return (
<View id="container" styleSheet={categoryStylesheet}>
<Button
id="anchor-heading"
cursor={CursorShape.PointingHandCursor}
on={{ MouseButtonRelease: goToGenre }}
text={name}
/>
<Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />
<View id="child-view">
{playlists?.map((playlist, index) => {
return (
<PlaylistCard
key={index + playlist.id}
id={playlist.id}
name={playlist.name}
thumbnail={playlist.images[0].url}
/>
);
return <PlaylistCard key={index + playlist.id} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
})}
</View>
</View>
@ -126,15 +93,14 @@ interface PlaylistCardProps {
id: string;
}
export const PlaylistCard = React.memo(
({ id, name, thumbnail }: PlaylistCardProps) => {
const history = useHistory();
export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardProps) => {
const history = useHistory();
function gotoPlaylist() {
history.push(`/playlist/${id}`, { name, thumbnail });
}
function gotoPlaylist() {
history.push(`/playlist/${id}`, { name, thumbnail });
}
const playlistStyleSheet = `
const playlistStyleSheet = `
#playlist-container{
max-width: 250px;
flex-direction: column;
@ -148,20 +114,9 @@ export const PlaylistCard = React.memo(
}
`;
return (
<View
id="playlist-container"
cursor={CursorShape.PointingHandCursor}
styleSheet={playlistStyleSheet}
on={{ MouseButtonRelease: gotoPlaylist }}
>
<CachedImage
src={thumbnail}
maxSize={{ height: 150, width: 150 }}
scaledContents
alt={name}
/>
</View>
);
}
);
return (
<View id="playlist-container" cursor={CursorShape.PointingHandCursor} styleSheet={playlistStyleSheet} on={{ MouseButtonRelease: gotoPlaylist }}>
<CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} />
</View>
);
});

View File

@ -0,0 +1,87 @@
import { ScrollArea, View } from "@nodegui/react-nodegui";
import React, { useContext } from "react";
import { Redirect, Route } from "react-router";
import { QueryCacheKeys } from "../conf";
import playerContext from "../context/playerContext";
import useSpotifyQuery from "../hooks/useSpotifyQuery";
import { PlaylistCard } from "./Home";
import { PlaylistSimpleControls, TrackButton } from "./PlaylistView";
import { TabMenuItem } from "./TabMenu";
function Library() {
return (
<View style="flex: 1; flex-direction: 'row';">
<Redirect from="/library" to="/library/saved-tracks" />
<View style="flex-direction: 'column'; flex: 1; max-width: 150px;">
<TabMenuItem title="Saved Tracks" url="/library/saved-tracks" />
<TabMenuItem title="Playlists" url="/library/playlists" />
</View>
<Route exact path="/library/saved-tracks">
<UserSavedTracks />
</Route>
<Route exact path="/library/playlists">
<UserPlaylists />
</Route>
</View>
);
}
export default Library;
function UserPlaylists() {
const { data: userPlaylists, isError, isLoading, refetch } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(QueryCacheKeys.userPlaylists, (spotifyApi) =>
spotifyApi.getUserPlaylists().then((userPlaylists) => {
return userPlaylists.body.items;
})
);
return (
<ScrollArea style="flex: 1; border: none;">
<View style="flex: 1; flex-direction: 'row'; flex-wrap: 'wrap'; justify-content: 'space-evenly'; width: 330px; align-items: 'center';">
{userPlaylists?.map((playlist, index) => (
<PlaylistCard key={index + playlist.id} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />
))}
</View>
</ScrollArea>
);
}
function UserSavedTracks() {
const userSavedPlaylistId = "user-saved-tracks";
const { data: userTracks, isError, isLoading } = useSpotifyQuery<SpotifyApi.SavedTrackObject[]>(QueryCacheKeys.userSavedTracks, (spotifyApi) =>
spotifyApi.getMySavedTracks({ limit: 50 }).then((tracks) => tracks.body.items)
);
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack, currentTrack } = useContext(playerContext);
function handlePlaylistPlayPause() {
if (currentPlaylist?.id !== userSavedPlaylistId && userTracks) {
setCurrentPlaylist({ id: userSavedPlaylistId, name: "Liked Tracks", thumbnail: "https://nerdist.com/wp-content/uploads/2020/07/maxresdefault.jpg", tracks: userTracks });
setCurrentTrack(userTracks[0].track);
} else {
setCurrentPlaylist(undefined);
setCurrentTrack(undefined);
}
}
return (
<View style="flex: 1; flex-direction: 'column';">
<PlaylistSimpleControls handlePlaylistPlayPause={handlePlaylistPlayPause} isActive={currentPlaylist?.id === userSavedPlaylistId} />
<ScrollArea style="flex: 1; border: none;">
<View style="flex: 1; flex-direction: 'column'; align-items: 'stretch';">
{userTracks?.map(({ track }) => (
<TrackButton
active={currentPlaylist?.id === userSavedPlaylistId && currentTrack?.id === track.id}
artist={track.artists.map((x) => x.name).join(", ")}
name={track.name}
on={{
clicked() {
setCurrentTrack(track);
},
}}
/>
))}
</View>
</ScrollArea>
</View>
);
}

View File

@ -6,11 +6,10 @@ import { shuffleArray } from "../helpers/shuffleArray";
import NodeMpv from "node-mpv";
import { getYoutubeTrack } from "../helpers/getYoutubeTrack";
import PlayerProgressBar from "./PlayerProgressBar";
import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegular } from "../icons";
import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegular, heart } from "../icons";
import IconButton from "./shared/IconButton";
import authContext from "../context/authContext";
import useSpotifyApi from "../hooks/useSpotifyApi";
import showError from "../helpers/showError";
import useTrackReaction from "../hooks/useTrackReaction";
export const audioPlayer = new NodeMpv(
{
@ -26,8 +25,7 @@ export const audioPlayer = new NodeMpv(
function Player(): ReactElement {
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
const spotifyApi = useSpotifyApi();
const { access_token } = useContext(authContext);
const { reactToTrack, isFavorite } = useTrackReaction();
const initVolume = parseFloat(localStorage.getItem("volume") ?? "55");
const [isPaused, setIsPaused] = useState(true);
const [volume, setVolume] = useState<number>(initVolume);
@ -60,7 +58,7 @@ function Player(): ReactElement {
}
} catch (error) {
console.error("Failed to start audio player", error);
showError(error, "[Failed starting audio player]: ")
showError(error, "[Failed starting audio player]: ");
}
})();
@ -71,19 +69,6 @@ function Player(): ReactElement {
};
}, []);
// useEffect(() => {
// (async () => {
// try {
// if (access_token) {
// const userSavedTrack = await spotifyApi.getMySavedTracks();
// console.log("userSavedTrack:", userSavedTrack);
// }
// } catch (error) {
// console.error("Failed to get spotify user saved tracks: ", error);
// }
// })();
// }, [access_token]);
// track change effect
useEffect(() => {
(async () => {
@ -143,14 +128,14 @@ function Player(): ReactElement {
};
const pauseListener = () => {
setIsPaused(true);
}
};
const resumeListener = () => {
setIsPaused(false);
};
audioPlayer.on("status", statusListener);
audioPlayer.on("stopped", stopListener);
audioPlayer.on("paused", pauseListener)
audioPlayer.on("resumed", resumeListener)
audioPlayer.on("paused", pauseListener);
audioPlayer.on("resumed", resumeListener);
return () => {
audioPlayer.off("status", statusListener);
audioPlayer.off("stopped", stopListener);
@ -189,13 +174,13 @@ function Player(): ReactElement {
await audioPlayer.stop();
}
} catch (error) {
showError(error, "[Failed at audio-player stop]: ")
showError(error, "[Failed at audio-player stop]: ");
}
}
const artistsNames = currentTrack?.artists?.map((x) => x.name);
return (
<GridView enabled={!!currentTrack} style="flex: 1; max-height: 100px;">
<GridView enabled={!!currentTrack} style="flex: 1; max-height: 120px;">
<GridRow>
<GridColumn width={2}>
<Text ref={titleRef} wordWrap>
@ -221,7 +206,16 @@ function Player(): ReactElement {
</GridColumn>
<GridColumn width={2}>
<BoxView>
<IconButton icon={new QIcon(heartRegular)} />
<IconButton
on={{
clicked() {
if (currentTrack) {
reactToTrack({ added_at: Date.now().toString(), track: currentTrack });
}
},
}}
icon={new QIcon(isFavorite(currentTrack?.id ?? "") ? heart : heartRegular)}
/>
<Slider minSize={{ height: 20, width: 80 }} maxSize={{ height: 20, width: 100 }} hasTracking sliderPosition={volume} on={volumeHandler} orientation={Orientation.Horizontal} />
</BoxView>
</GridColumn>

View File

@ -1,8 +1,8 @@
import React, { FC, useContext } from "react";
import { BoxView, View, Button, ScrollArea, Text } from "@nodegui/react-nodegui";
import { View, Button, ScrollArea, Text } from "@nodegui/react-nodegui";
import BackButton from "./BackButton";
import { useLocation, useParams } from "react-router";
import { Direction, QAbstractButtonSignals, QIcon } from "@nodegui/nodegui";
import { QAbstractButtonSignals, QIcon } from "@nodegui/nodegui";
import { WidgetEventListeners } from "@nodegui/react-nodegui/dist/components/View/RNView";
import playerContext from "../context/playerContext";
import IconButton from "./shared/IconButton";
@ -44,14 +44,10 @@ const PlaylistView: FC = () => {
};
return (
<View style={`flex: 1; flex-direction: 'column'; flex-grow: 1;`}>
<View style={`justify-content: 'space-evenly'; max-width: 150px; padding: 10px;`}>
<BackButton />
<IconButton icon={new QIcon(heartRegular)} />
<IconButton style={`background-color: #00be5f; color: white;`} on={{ clicked: handlePlaylistPlayPause }} icon={new QIcon(currentPlaylist?.id === params.id ? stop : play)} />
</View>
<View style={`flex: 1; flex-direction: 'column';`}>
<PlaylistSimpleControls handlePlaylistPlayPause={handlePlaylistPlayPause} isActive={currentPlaylist?.id === params.id} />
<Text>{`<center><h2>${location.state.name[0].toUpperCase()}${location.state.name.slice(1)}</h2></center>`}</Text>
<ScrollArea style={`flx:1; flex-grow: 1; border: none;`}>
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
<View style={`flex-direction:column; flex: 1;`}>
{isLoading && <Text>{`Loading Tracks...`}</Text>}
{isError && (
@ -70,7 +66,7 @@ const PlaylistView: FC = () => {
{tracks?.map(({ track }, index) => {
return (
<TrackButton
key={index+track.id}
key={index + track.id}
active={currentTrack?.id === track.id && currentPlaylist?.id === params.id}
artist={track.artists.map((x) => x.name).join(", ")}
name={track.name}
@ -84,14 +80,14 @@ const PlaylistView: FC = () => {
);
};
interface TrackButtonProps {
export interface TrackButtonProps {
name: string;
artist: string;
active: boolean;
on: Partial<QAbstractButtonSignals | WidgetEventListeners>;
}
const TrackButton: FC<TrackButtonProps> = ({ name, artist, on, active }) => {
export const TrackButton: FC<TrackButtonProps> = ({ name, artist, on, active }) => {
return <Button id={`${active ? "active" : ""}`} on={on} text={`${name} -- ${artist}`} styleSheet={trackButtonStyle} />;
};
@ -103,3 +99,18 @@ const trackButtonStyle = `
`;
export default PlaylistView;
interface PlaylistSimpleControlsProps {
handlePlaylistPlayPause: () => void;
isActive: boolean;
}
export function PlaylistSimpleControls({ handlePlaylistPlayPause, isActive }: PlaylistSimpleControlsProps) {
return (
<View style={`justify-content: 'space-evenly'; max-width: 150px; padding: 10px;`}>
<BackButton />
<IconButton icon={new QIcon(heartRegular)} />
<IconButton style={`background-color: #00be5f; color: white;`} on={{ clicked: handlePlaylistPlayPause }} icon={new QIcon(isActive ? stop : play)} />
</View>
);
}

View File

@ -1,26 +1,26 @@
import React from "react";
import {Route} from "react-router"
import {View, Button} from "@nodegui/react-nodegui"
import {QIcon} from "@nodegui/nodegui"
import { View, Button, Text } from "@nodegui/react-nodegui";
import { useHistory, useLocation } from "react-router";
function TabMenu(){
return (
function TabMenu() {
return (
<View id="tabmenu" styleSheet={tabBarStylesheet}>
<TabMenuItem title="Browse"/>
<TabMenuItem title="Library"/>
<TabMenuItem title="Currently Playing"/>
<View>
<Text>{`<h1>Spotube</h1>`}</Text>
</View>
<TabMenuItem url="/home" title="Browse" />
<TabMenuItem url="/library" title="Library" />
<TabMenuItem url="/currently" title="Currently Playing" />
</View>
)
);
}
const tabBarStylesheet = `
export const tabBarStylesheet = `
#tabmenu{
flex-direction: 'column';
align-items: 'center';
max-width: 225px;
}
#tabmenu-item{
background-color: transparent;
padding: 10px;
flex-direction: 'row';
justify-content: 'space-around';
}
#tabmenu-item:hover{
color: green;
@ -28,20 +28,30 @@ const tabBarStylesheet = `
#tabmenu-item:active{
color: #59ff88;
}
`
#tabmenu-active-item{
background-color: green;
color: white;
}
`;
export default TabMenu;
interface TabMenuItemProps{
export interface TabMenuItemProps {
title: string;
url: string;
/**
* path to the icon in string
*/
icon?: string;
}
export function TabMenuItem({icon, title}:TabMenuItemProps){
return (
<Button id="tabmenu-item" text={title}/>
)
export function TabMenuItem({ icon, title, url }: TabMenuItemProps) {
const location = useLocation();
const history = useHistory();
function clicked() {
history.push(url);
}
return <Button on={{ clicked }} id={location.pathname.replace("/", " ").startsWith(url.replace("/", " ")) ? "tabmenu-active-item" : `tabmenu-item`} text={title} />;
}

View File

@ -11,4 +11,6 @@ export enum QueryCacheKeys{
categoryPlaylists = "categoryPlaylists",
genrePlaylists="genrePlaylists",
playlistTracks="playlistTracks",
userPlaylists = "user-palylists",
userSavedTracks = "user-saved-tracks"
}

View File

@ -6,16 +6,6 @@ export interface AuthContext {
clientId: string;
clientSecret: string;
access_token: string;
/**
* the time when the current access token will expire \
* always update this with `Date.now() + expires_in`
*/
expires_in: number;
/**
* sets the time when the current access token will expire \
* always update this with `Date.now() + expires_in`
*/
setExpires_in: (arg: number)=>void;
setAccess_token: Dispatch<SetStateAction<string>>;
}
@ -23,10 +13,8 @@ const authContext = React.createContext<AuthContext>({
isLoggedIn: false,
setIsLoggedIn() {},
access_token: "",
expires_in: 0,
clientId: "",
clientSecret: "",
setExpires_in() {},
setAccess_token() {},
});

View File

@ -2,7 +2,7 @@ import React, { Dispatch, SetStateAction } from "react";
export type CurrentTrack = SpotifyApi.TrackObjectFull;
export type CurrentPlaylist = { tracks: SpotifyApi.PlaylistTrackObject[]; id: string; name: string; thumbnail: string };
export type CurrentPlaylist = { tracks: (SpotifyApi.PlaylistTrackObject | SpotifyApi.SavedTrackObject)[]; id: string; name: string; thumbnail: string };
export interface PlayerContext {
currentPlaylist?: CurrentPlaylist;

View File

@ -0,0 +1,24 @@
import { useEffect } from "react";
import { useMutation, UseMutationOptions } from "react-query";
import SpotifyWebApi from "spotify-web-api-node";
import useSpotifyApi from "./useSpotifyApi";
import useSpotifyApiError from "./useSpotifyApiError";
type SpotifyMutationFn<TData = unknown, TVariables = unknown> = (spotifyApi: SpotifyWebApi, variables: TVariables) => Promise<TData>;
function useSpotifyMutation<TData = unknown, TVariable = unknown>(mutationFn: SpotifyMutationFn<TData, TVariable>, options?: UseMutationOptions<TData, SpotifyApi.ErrorObject, TVariable>) {
const spotifyApi = useSpotifyApi();
const handleSpotifyError = useSpotifyApiError(spotifyApi);
const mutation = useMutation<TData, SpotifyApi.ErrorObject, TVariable>((arg) => mutationFn(spotifyApi, arg), options);
const { isError, error } = mutation;
useEffect(() => {
if (isError && error) {
handleSpotifyError(error);
}
}, [isError, error]);
return mutation;
}
export default useSpotifyMutation;

View File

@ -0,0 +1,31 @@
import { useQueryClient } from "react-query";
import { QueryCacheKeys } from "../conf";
import useSpotifyMutation from "./useSpotifyMutation";
import useSpotifyQuery from "./useSpotifyQuery";
function useTrackReaction() {
const queryClient = useQueryClient();
const { data: favoriteTracks } = useSpotifyQuery<SpotifyApi.SavedTrackObject[]>(QueryCacheKeys.userSavedTracks, (spotifyApi) =>
spotifyApi.getMySavedTracks({ limit: 50 }).then((tracks) => tracks.body.items)
);
const { mutate: reactToTrack } = useSpotifyMutation<{}, SpotifyApi.SavedTrackObject>(
(spotifyApi, { track }) => spotifyApi[isFavorite(track.id) ? "removeFromMySavedTracks" : "addToMySavedTracks"]([track.id]).then((res) => res.body),
{
onSuccess(_, track) {
queryClient.setQueryData<SpotifyApi.SavedTrackObject[]>(
QueryCacheKeys.userSavedTracks,
isFavorite(track.track.id) ? (old) => (old ?? []).filter((oldTrack) => oldTrack.track.id !== track.track.id) : (old) => [...(old ?? []), track]
);
},
}
);
const favoriteTrackIds = favoriteTracks?.map((track) => track.track.id);
function isFavorite(trackId: string) {
return favoriteTrackIds?.includes(trackId);
}
return { reactToTrack, isFavorite, favoriteTracks };
}
export default useTrackReaction;

View File

@ -1,30 +1,42 @@
import React, { useContext } from "react";
import {View} from "@nodegui/react-nodegui";
import { Route } from "react-router";
import { Redirect, Route } from "react-router";
import authContext from "./context/authContext";
import Home from "./components/Home";
import Login from "./components/Login";
import PlaylistView from "./components/PlaylistView";
import PlaylistGenreView from "./components/PlaylistGenreView";
import TabMenu from "./components/TabMenu";
import CurrentPlaylist from "./components/CurrentPlaylist";
import Library from "./components/Library";
function Routes() {
const {
isLoggedIn
} = useContext(authContext);
const { isLoggedIn } = useContext(authContext);
return (
<>
<Route path="/">
{ isLoggedIn ?
<View style="background-color: black; flex: 1; flex-direction: 'column';">
<TabMenu />
<Route exact path="/"><Home/></Route>
<Route exact path="/playlist/:id"><PlaylistView /></Route>
<Route exact path="/genre/playlists/:id"><PlaylistGenreView /></Route>
</View>
: <Login/>
}
{isLoggedIn ? (
<>
<Redirect from="/" to="/home" />
<TabMenu />
<Route exact path="/home">
<Home />
</Route>
<Route exact path="/playlist/:id">
<PlaylistView />
</Route>
<Route exact path="/genre/playlists/:id">
<PlaylistGenreView />
</Route>
</>
) : (
<Login />
)}
</Route>
<Route path="/currently">
<CurrentPlaylist />
</Route>
<Route path="/library">
<Library />
</Route>
</>
);