mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
Tabbed Interface with support for current playback & user library added
This commit is contained in:
parent
fe39ab0ffd
commit
247f1b563b
BIN
assets/rickroll.jpg
Normal file
BIN
assets/rickroll.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
7501
package-lock.json
generated
7501
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
src/app.tsx
14
src/app.tsx
@ -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>
|
||||
|
35
src/components/CurrentPlaylist.tsx
Normal file
35
src/components/CurrentPlaylist.tsx
Normal 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;
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
87
src/components/Library.tsx
Normal file
87
src/components/Library.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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} />;
|
||||
}
|
||||
|
@ -11,4 +11,6 @@ export enum QueryCacheKeys{
|
||||
categoryPlaylists = "categoryPlaylists",
|
||||
genrePlaylists="genrePlaylists",
|
||||
playlistTracks="playlistTracks",
|
||||
userPlaylists = "user-palylists",
|
||||
userSavedTracks = "user-saved-tracks"
|
||||
}
|
@ -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() {},
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
|
24
src/hooks/useSpotifyMutation.ts
Normal file
24
src/hooks/useSpotifyMutation.ts
Normal 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;
|
31
src/hooks/useTrackReaction.ts
Normal file
31
src/hooks/useTrackReaction.ts
Normal 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;
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user