Fixed unintentional Home component render on track change, implemented new mechanism with caching for spotifyAPI using react-query & refresh access token method. Moved to BoxView from View for frame bugs of react-nodegui

This commit is contained in:
krtirtho 2021-03-01 22:47:15 +06:00
parent 5a0486bc4f
commit faea24e86e
15 changed files with 478 additions and 8150 deletions

12
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"problemMatcher": [],
"label": "npm: start",
"detail": "qode ./dist/index.js"
}
]
}

8218
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@
"build": "webpack --mode=production", "build": "webpack --mode=production",
"dev": "TSC_WATCHFILE=UseFsEvents webpack --mode=development", "dev": "TSC_WATCHFILE=UseFsEvents webpack --mode=development",
"start": "qode ./dist/index.js", "start": "qode ./dist/index.js",
"start:trace": "qode ./dist/index.js --trace",
"debug": "qode --inspect ./dist/index.js" "debug": "qode --inspect ./dist/index.js"
}, },
"dependencies": { "dependencies": {
@ -16,6 +17,7 @@
"@nodegui/react-nodegui": "^0.10.0", "@nodegui/react-nodegui": "^0.10.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"base64url": "^3.0.1", "base64url": "^3.0.1",
"chalk": "^4.1.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"du": "^1.0.0", "du": "^1.0.0",
"express": "^4.17.1", "express": "^4.17.1",
@ -25,9 +27,12 @@
"node-mpv": "^2.0.0-beta.1", "node-mpv": "^2.0.0-beta.1",
"open": "^7.4.1", "open": "^7.4.1",
"react": "^16.14.0", "react": "^16.14.0",
"react-dom": "^17.0.1",
"react-query": "^3.12.0",
"react-router": "^5.2.0", "react-router": "^5.2.0",
"scrape-yt": "^1.4.7", "scrape-yt": "^1.4.7",
"spotify-web-api-node": "^5.0.2" "spotify-web-api-node": "^5.0.2",
"uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.11.6", "@babel/core": "^7.11.6",
@ -43,6 +48,7 @@
"@types/react": "^16.9.49", "@types/react": "^16.9.49",
"@types/react-router": "^5.1.11", "@types/react-router": "^5.1.11",
"@types/spotify-web-api-node": "^5.0.0", "@types/spotify-web-api-node": "^5.0.0",
"@types/uuid": "^8.3.0",
"@types/webpack-env": "^1.15.3", "@types/webpack-env": "^1.15.3",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { Window, hot, View, useEventHandler } from "@nodegui/react-nodegui"; import { Window, hot, View, useEventHandler, BoxView } from "@nodegui/react-nodegui";
import { QIcon, QKeyEvent, QMainWindow, QMainWindowSignals, WidgetEventTypes, WindowState } from "@nodegui/nodegui"; import { Direction, QIcon, QKeyEvent, QMainWindow, QMainWindowSignals, WidgetEventTypes, WindowState } from "@nodegui/nodegui";
import nodeguiIcon from "../assets/nodegui.jpg"; import nodeguiIcon from "../assets/nodegui.jpg";
import { MemoryRouter } from "react-router"; import { MemoryRouter } from "react-router";
import Routes from "./routes"; import Routes from "./routes";
@ -8,9 +8,11 @@ import { LocalStorage } from "node-localstorage";
import authContext from "./context/authContext"; import authContext from "./context/authContext";
import playerContext, { CurrentPlaylist, CurrentTrack } from "./context/playerContext"; import playerContext, { CurrentPlaylist, CurrentTrack } from "./context/playerContext";
import Player, { audioPlayer } from "./components/Player"; import Player, { audioPlayer } from "./components/Player";
import { QueryClient, QueryClientProvider } from "react-query";
import express from "express"; import express from "express";
import open from "open"; import open from "open";
import spotifyApi from "./initializations/spotifyApi"; import spotifyApi from "./initializations/spotifyApi";
import showError from "./helpers/showError";
export enum CredentialKeys { export enum CredentialKeys {
credentials = "credentials", credentials = "credentials",
@ -25,6 +27,15 @@ export interface Credentials {
const minSize = { width: 700, height: 750 }; const minSize = { width: 700, height: 750 };
const winIcon = new QIcon(nodeguiIcon); const winIcon = new QIcon(nodeguiIcon);
global.localStorage = new LocalStorage("./local"); global.localStorage = new LocalStorage("./local");
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError(error) {
showError(error);
},
},
},
});
function RootApp() { function RootApp() {
const windowRef = useRef<QMainWindow>(); const windowRef = useRef<QMainWindow>();
@ -66,10 +77,9 @@ function RootApp() {
const [expires_in, setExpires_in] = useState<number>(0); const [expires_in, setExpires_in] = useState<number>(0);
const [access_token, setAccess_token] = useState<string>(""); const [access_token, setAccess_token] = useState<string>("");
const [currentPlaylist, setCurrentPlaylist] = useState<CurrentPlaylist>(); const [currentPlaylist, setCurrentPlaylist] = useState<CurrentPlaylist>();
const cachedCredentials = localStorage.getItem(CredentialKeys.credentials); const cachedCredentials = localStorage.getItem(CredentialKeys.credentials);
const setExpireTime = (expirationDuration: number) => setExpires_in(Date.now() + expirationDuration); const setExpireTime = (expirationDuration: number) => setExpires_in(Date.now() + expirationDuration * 1000 /* 1s = 1000 ms */);
useEffect(() => { useEffect(() => {
setIsLoggedIn(!!cachedCredentials); setIsLoggedIn(!!cachedCredentials);
@ -128,10 +138,14 @@ function RootApp() {
<MemoryRouter> <MemoryRouter>
<authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, expires_in, setAccess_token, setExpires_in: setExpireTime, ...credentials }}> <authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, expires_in, setAccess_token, setExpires_in: setExpireTime, ...credentials }}>
<playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}> <playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}>
<View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}> <QueryClientProvider client={queryClient}>
<Routes /> {/* <View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}> */}
{isLoggedIn && <Player />} <BoxView direction={Direction.TopToBottom}>
</View> <Routes />
{isLoggedIn && <Player />}
</BoxView>
{/* </View> */}
</QueryClientProvider>
</playerContext.Provider> </playerContext.Provider>
</authContext.Provider> </authContext.Provider>
</MemoryRouter> </MemoryRouter>

View File

@ -1,39 +1,38 @@
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import { Button, View, ScrollArea } from "@nodegui/react-nodegui"; import { Button, ScrollArea, BoxView } from "@nodegui/react-nodegui";
import playerContext from "../context/playerContext";
import authContext from "../context/authContext";
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import CachedImage from "./shared/CachedImage"; import CachedImage from "./shared/CachedImage";
import { CursorShape } from "@nodegui/nodegui"; import { CursorShape, Direction } from "@nodegui/nodegui";
import useSpotifyApi from "../hooks/useSpotifyApi"; import useSpotifyApi from "../hooks/useSpotifyApi";
import showError from "../helpers/showError";
import authContext from "../context/authContext";
import useSpotifyApiError from "../hooks/useSpotifyApiError";
function Home() { function Home() {
const { currentPlaylist } = useContext(playerContext);
const spotifyApi = useSpotifyApi(); const spotifyApi = useSpotifyApi();
const { access_token } = useContext(authContext); const { access_token } = useContext(authContext);
const [categories, setCategories] = useState<SpotifyApi.CategoryObject[]>([]); const [categories, setCategories] = useState<SpotifyApi.CategoryObject[]>([]);
const handleSpotifyError = useSpotifyApiError(spotifyApi);
useEffect(() => { useEffect(() => {
if (access_token) { if (categories.length === 0) {
(async () => { spotifyApi
try { .getCategories({ country: "US" })
const categoriesReceived = await spotifyApi.getCategories({ country: "US" }); .then((categoriesReceived) => setCategories(categoriesReceived.body.categories.items))
setCategories(categoriesReceived.body.categories.items); .catch((error) => {
} catch (error) { showError(error, "[Spotify genre loading failed]: ");
console.error("Spotify featured playlist loading failed: ", error); handleSpotifyError(error);
} });
})();
} }
}, [access_token]); }, [access_token]);
return ( return (
<ScrollArea style={`flex-grow: 1; border: none; flex: 1;`}> <ScrollArea style={`flex-grow: 1; border: none;`}>
<View style={`flex-direction: 'column'; justify-content: 'center'; flex: 1;`}> <BoxView direction={Direction.TopToBottom}>
{currentPlaylist && <CategoryCard key={((Math.random() * Date.now()) / Math.random()) * 100} id="current" name="Currently Playing" />}
{categories.map((category, index) => { {categories.map((category, index) => {
return <CategoryCard key={((index * Date.now()) / Math.random()) * 100} {...category} />; return <CategoryCard key={index+category.id} id={category.id} name={category.name} />;
})} })}
</View> </BoxView>
</ScrollArea> </ScrollArea>
); );
} }
@ -45,39 +44,42 @@ interface CategoryCardProps {
name: string; name: string;
} }
function CategoryCard({ id, name }: CategoryCardProps) { const CategoryCard = ({ id, name }: CategoryCardProps) => {
const history = useHistory(); const history = useHistory();
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]); const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
const { currentPlaylist } = useContext(playerContext);
const spotifyApi = useSpotifyApi(); const spotifyApi = useSpotifyApi();
const handleSpotifyError = useSpotifyApiError(spotifyApi);
useEffect(() => { useEffect(() => {
let mounted = true; if (playlists.length === 0) {
spotifyApi
(async () => { .getPlaylistsForCategory(id, { limit: 4 })
try { .then((playlistsRes) => setPlaylists(playlistsRes.body.playlists.items))
if (id !== "current") { .catch((error) => {
const playlistsRes = await spotifyApi.getPlaylistsForCategory(id, { limit: 4 }); showError(error, `[Failed to get playlists of category ${name}]: `);
mounted && setPlaylists(playlistsRes.body.playlists.items); handleSpotifyError(error);
} });
} catch (error) { }
console.error(`Failed to get playlists of category ${name} for: `, error);
}
})();
return () => {
mounted = false;
};
}, []); }, []);
function goToGenre() { function goToGenre() {
history.push(`/genre/playlists/${id}`, { name }); history.push(`/genre/playlists/${id}`, { name });
} }
const categoryStylesheet = ` return (
<BoxView id="container" styleSheet={categoryStylesheet} direction={Direction.TopToBottom}>
<Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />
<BoxView direction={Direction.LeftToRight}>
{playlists.map((playlist, index) => {
return <PlaylistCard key={index+playlist.id} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
})}
</BoxView>
</BoxView>
);
};
const categoryStylesheet = `
#container{ #container{
flex: 1;
flex-direction: column;
justify-content: 'center';
margin-bottom: 20px; margin-bottom: 20px;
} }
#anchor-heading{ #anchor-heading{
@ -87,41 +89,22 @@ function CategoryCard({ id, name }: CategoryCardProps) {
outline: none; outline: none;
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
align-self: 'flex-start'; text-align: left;
} }
#anchor-heading:hover{ #anchor-heading:hover{
border: none; border: none;
outline: none; outline: none;
text-decoration: underline; text-decoration: underline;
} }
#child-view{
flex: 1;
justify-content: 'space-evenly';
align-items: 'center';
flex-wrap: 'wrap';
}
`; `;
return (
<View id="container" styleSheet={categoryStylesheet}>
{(playlists.length > 0 || id === "current") && <Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />}
<View id="child-view">
{id === "current" && currentPlaylist && <PlaylistCard key={(Date.now() / Math.random()) * 100} {...currentPlaylist} />}
{playlists.map((playlist, index) => {
return <PlaylistCard key={((index * Date.now()) / Math.random()) * 100} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
})}
</View>
</View>
);
}
interface PlaylistCardProps { interface PlaylistCardProps {
thumbnail: string; thumbnail: string;
name: string; name: string;
id: string; id: string;
} }
export function PlaylistCard({ id, name, thumbnail }: PlaylistCardProps) { export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardProps) => {
const history = useHistory(); const history = useHistory();
function gotoPlaylist() { function gotoPlaylist() {
@ -130,9 +113,9 @@ export function PlaylistCard({ id, name, thumbnail }: PlaylistCardProps) {
const playlistStyleSheet = ` const playlistStyleSheet = `
#playlist-container{ #playlist-container{
max-width: 250px; max-width: 150px;
flex-direction: column; max-height: 150px;
padding: 2px; min-height: 150px;
} }
#playlist-container:hover{ #playlist-container:hover{
border: 1px solid green; border: 1px solid green;
@ -143,8 +126,8 @@ export function PlaylistCard({ id, name, thumbnail }: PlaylistCardProps) {
`; `;
return ( return (
<View id="playlist-container" cursor={CursorShape.PointingHandCursor} styleSheet={playlistStyleSheet} on={{ MouseButtonRelease: gotoPlaylist }}> <BoxView size={{height: 150, width: 150, fixed: true}} direction={Direction.TopToBottom} id="playlist-container" cursor={CursorShape.PointingHandCursor} styleSheet={playlistStyleSheet} on={{ MouseButtonRelease: gotoPlaylist }}>
<CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} /> <CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} />
</View> </BoxView>
); );
} });

View File

@ -10,6 +10,7 @@ import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegul
import IconButton from "./shared/IconButton"; import IconButton from "./shared/IconButton";
import authContext from "../context/authContext"; import authContext from "../context/authContext";
import useSpotifyApi from "../hooks/useSpotifyApi"; import useSpotifyApi from "../hooks/useSpotifyApi";
import showError from "../helpers/showError";
export const audioPlayer = new NodeMpv( export const audioPlayer = new NodeMpv(
{ {
@ -59,6 +60,7 @@ function Player(): ReactElement {
} }
} catch (error) { } catch (error) {
console.error("Failed to start audio player", error); console.error("Failed to start audio player", error);
showError(error, "[Failed starting audio player]: ")
} }
})(); })();
@ -98,7 +100,7 @@ function Player(): ReactElement {
setIsStopped(true); setIsStopped(true);
setIsPaused(true); setIsPaused(true);
} }
console.error(error); showError(error, "[Failure at track change]: ");
} }
})(); })();
}, [currentTrack]); }, [currentTrack]);
@ -170,14 +172,13 @@ function Player(): ReactElement {
setIsPaused(true); setIsPaused(true);
} }
} catch (error) { } catch (error) {
console.error(error); showError(error, "[Track control failed]: ");
} }
}; };
const prevOrNext = (constant: number) => { const prevOrNext = (constant: number) => {
if (currentTrack && playlistTracksIds && currentPlaylist) { if (currentTrack && playlistTracksIds && currentPlaylist) {
const index = playlistTracksIds.indexOf(currentTrack.id) + constant; const index = playlistTracksIds.indexOf(currentTrack.id) + constant;
console.log("index:", index);
setCurrentTrack(currentPlaylist.tracks[index > playlistTracksIds?.length - 1 ? 0 : index < 0 ? playlistTracksIds.length - 1 : index].track); setCurrentTrack(currentPlaylist.tracks[index > playlistTracksIds?.length - 1 ? 0 : index < 0 ? playlistTracksIds.length - 1 : index].track);
} }
}; };
@ -188,7 +189,7 @@ function Player(): ReactElement {
await audioPlayer.stop(); await audioPlayer.stop();
} }
} catch (error) { } catch (error) {
console.error("Failed to stop the audio player: ", error); showError(error, "[Failed at audio-player stop]: ")
} }
} }

View File

@ -2,7 +2,9 @@ import { ScrollArea, Text, View } from "@nodegui/react-nodegui";
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import { useLocation, useParams } from "react-router"; import { useLocation, useParams } from "react-router";
import authContext from "../context/authContext"; import authContext from "../context/authContext";
import showError from "../helpers/showError";
import useSpotifyApi from "../hooks/useSpotifyApi"; import useSpotifyApi from "../hooks/useSpotifyApi";
import useSpotifyApiError from "../hooks/useSpotifyApiError";
import BackButton from "./BackButton"; import BackButton from "./BackButton";
import { PlaylistCard } from "./Home"; import { PlaylistCard } from "./Home";
@ -12,23 +14,18 @@ function PlaylistGenreView() {
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]); const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
const { access_token, isLoggedIn } = useContext(authContext); const { access_token, isLoggedIn } = useContext(authContext);
const spotifyApi = useSpotifyApi(); const spotifyApi = useSpotifyApi();
const handleSpotifyError = useSpotifyApiError(spotifyApi);
useEffect(() => { useEffect(() => {
let mounted = true; if (playlists.length === 0 && access_token) {
spotifyApi
(async () => { .getPlaylistsForCategory(id)
try { .then((playlistsRes) => setPlaylists(playlistsRes.body.playlists.items))
if (access_token) { .catch((error) => {
const playlistsRes = await spotifyApi.getPlaylistsForCategory(id); showError(error, `[Failed to get playlists of category ${location.state.name} for]: `);
mounted && setPlaylists(playlistsRes.body.playlists.items); handleSpotifyError(error);
} });
} catch (error) { }
console.error(`Failed to get playlists of category ${name} for: `, error);
}
})();
return () => {
mounted = false;
};
}, [access_token]); }, [access_token]);
const playlistGenreViewStylesheet = ` const playlistGenreViewStylesheet = `

View File

@ -1,15 +1,15 @@
import React, { FC, useContext, useEffect, useState } from "react"; import React, { FC, useContext } from "react";
import { Button, ScrollArea, Text, View } from "@nodegui/react-nodegui"; import { BoxView, Button, ScrollArea, Text } from "@nodegui/react-nodegui";
import BackButton from "./BackButton"; import BackButton from "./BackButton";
import { useLocation, useParams } from "react-router"; import { useLocation, useParams } from "react-router";
import { QAbstractButtonSignals, QIcon } from "@nodegui/nodegui"; import { Direction, QAbstractButtonSignals, QIcon } from "@nodegui/nodegui";
import { WidgetEventListeners } from "@nodegui/react-nodegui/dist/components/View/RNView"; import { WidgetEventListeners } from "@nodegui/react-nodegui/dist/components/View/RNView";
import authContext from "../context/authContext";
import playerContext from "../context/playerContext"; import playerContext from "../context/playerContext";
import IconButton from "./shared/IconButton"; import IconButton from "./shared/IconButton";
import { heartRegular, play, stop } from "../icons"; import { heartRegular, play, stop } from "../icons";
import { audioPlayer } from "./Player"; import { audioPlayer } from "./Player";
import useSpotifyApi from "../hooks/useSpotifyApi"; import { QueryCacheKeys } from "../conf";
import useSpotifyQuery from "../hooks/useSpotifyQuery";
export interface PlaylistTrackRes { export interface PlaylistTrackRes {
name: string; name: string;
@ -18,28 +18,18 @@ export interface PlaylistTrackRes {
} }
const PlaylistView: FC = () => { const PlaylistView: FC = () => {
const { isLoggedIn } = useContext(authContext);
const { setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext); const { setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext);
const spotifyApi = useSpotifyApi();
const params = useParams<{ id: string }>(); const params = useParams<{ id: string }>();
const location = useLocation<{ name: string; thumbnail: string }>(); const location = useLocation<{ name: string; thumbnail: string }>();
const [tracks, setTracks] = useState<SpotifyApi.PlaylistTrackObject[]>([]);
useEffect(() => { const { data: tracks, isSuccess, isError, isLoading, refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>(
if (isLoggedIn) { [QueryCacheKeys.playlistTracks, params.id],
(async () => { (spotifyApi) => spotifyApi.getPlaylistTracks(params.id).then((track) => track.body.items),
try { { initialData: [] }
const tracks = await spotifyApi.getPlaylistTracks(params.id); );
setTracks(tracks.body.items);
} catch (error) {
console.error(`Failed to get tracks from ${params.id}: `, error);
}
})();
}
}, []);
const handlePlaylistPlayPause = () => { const handlePlaylistPlayPause = () => {
if (currentPlaylist?.id !== params.id) { if (currentPlaylist?.id !== params.id && isSuccess && tracks) {
setCurrentPlaylist({ ...params, ...location.state, tracks }); setCurrentPlaylist({ ...params, ...location.state, tracks });
setCurrentTrack(tracks[0].track); setCurrentTrack(tracks[0].track);
} else { } else {
@ -49,42 +39,48 @@ const PlaylistView: FC = () => {
} }
}; };
const trackClickHandler = async (track: SpotifyApi.TrackObjectFull) => { const trackClickHandler = (track: SpotifyApi.TrackObjectFull) => {
try { setCurrentTrack(track);
setCurrentTrack(track);
} catch (error) {
console.error("Failed to resolve track's youtube url: ", error);
}
}; };
return ( return (
<View style={`flex-direction: 'column'; flex-grow: 1;`}> <BoxView direction={Direction.TopToBottom}>
<View style={`justify-content: 'space-between'; padding-bottom: 10px; padding-left: 10px;`}> <BoxView style={`max-width: 150px;`}>
<BackButton /> <BackButton />
<View style={`height: 50px; justify-content: 'space-between'; width: 100px; padding-right: 20px;`}> <IconButton icon={new QIcon(heartRegular)} />
<IconButton icon={new QIcon(heartRegular)} /> <IconButton style={`background-color: #00be5f; color: white;`} on={{ clicked: handlePlaylistPlayPause }} icon={new QIcon(currentPlaylist?.id === params.id ? stop : play)} />
<IconButton style={`background-color: #00be5f; color: white;`} on={{ clicked: handlePlaylistPlayPause }} icon={new QIcon(currentPlaylist?.id === params.id ? stop : play)} /> </BoxView>
</View>
</View>
<Text>{`<center><h2>${location.state.name[0].toUpperCase()}${location.state.name.slice(1)}</h2></center>`}</Text> <Text>{`<center><h2>${location.state.name[0].toUpperCase()}${location.state.name.slice(1)}</h2></center>`}</Text>
<ScrollArea style={`flex-grow: 1; border: none;`}> <ScrollArea style={`flex-grow: 1; border: none;`}>
<View style={`flex-direction:column;`}> <BoxView /* style={`flex-direction:column;`} */ direction={Direction.TopToBottom}>
{isLoggedIn && {isLoading && <Text>{`Loading Tracks...`}</Text>}
tracks.length > 0 && {isError && (
tracks.map(({ track }, index) => { <>
return ( <Text>{`Failed to load ${location.state.name} tracks`}</Text>
<TrackButton <Button
key={index * ((Date.now() / Math.random()) * 100)} on={{
active={currentTrack?.id === track.id && currentPlaylist?.id === params.id} clicked() {
artist={track.artists.map((x) => x.name).join(", ")} refetch();
name={track.name} },
on={{ clicked: () => trackClickHandler(track) }} }}
/> text="Retry"
); />
})} </>
</View> )}
{tracks?.map(({ track }, index) => {
return (
<TrackButton
key={index+track.id}
active={currentTrack?.id === track.id && currentPlaylist?.id === params.id}
artist={track.artists.map((x) => x.name).join(", ")}
name={track.name}
on={{ clicked: () => trackClickHandler(track) }}
/>
);
})}
</BoxView>
</ScrollArea> </ScrollArea>
</View> </BoxView>
); );
}; };

View File

@ -3,6 +3,7 @@ import { Image, Text, View } from "@nodegui/react-nodegui";
import { QLabel } from "@nodegui/nodegui"; import { QLabel } from "@nodegui/nodegui";
import { ImageProps } from "@nodegui/react-nodegui/dist/components/Image/RNImage"; import { ImageProps } from "@nodegui/react-nodegui/dist/components/Image/RNImage";
import { getCachedImageBuffer } from "../../helpers/getCachedImageBuffer"; import { getCachedImageBuffer } from "../../helpers/getCachedImageBuffer";
import showError from "../../helpers/showError";
interface CachedImageProps extends Omit<ImageProps, "buffer"> { interface CachedImageProps extends Omit<ImageProps, "buffer"> {
src: string; src: string;
@ -15,18 +16,17 @@ function CachedImage({ src, alt, ...props }: CachedImageProps) {
const [imageProcessError, setImageProcessError] = useState<boolean>(false); const [imageProcessError, setImageProcessError] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
let mounted = true; if (imageBuffer===undefined) {
(async () => { getCachedImageBuffer(src, props.maxSize ?? props.size)
try { .then((buffer) => setImageBuffer(buffer))
mounted && setImageBuffer(await getCachedImageBuffer(src, props.maxSize ?? props.size)); .catch((error) => {
} catch (error) { setImageProcessError(false);
mounted && setImageProcessError(false); showError(error, "[Cached Image Error]: ");
console.log("Cached Image Error:", error); });
} }
})();
return () => { return () => {
imgRef.current?.close(); imgRef.current?.close();
mounted = false;
}; };
}, []); }, []);
return !imageProcessError && imageBuffer ? ( return !imageProcessError && imageBuffer ? (

View File

@ -3,4 +3,11 @@ import { join } from "path";
const env = dotenv.config({path: join(process.cwd(), ".env")}).parsed as any const env = dotenv.config({path: join(process.cwd(), ".env")}).parsed as any
export const clientId = ""; export const clientId = "";
export const trace = process.argv.find(arg => arg === "--trace") ?? false;
export const redirectURI = "http://localhost:4304/auth/spotify/callback" export const redirectURI = "http://localhost:4304/auth/spotify/callback"
export enum QueryCacheKeys{
categories="categories",
categoryPlaylists = "categoryPlaylists",
playlistTracks="playlistTracks"
}

8
src/helpers/showError.ts Normal file
View File

@ -0,0 +1,8 @@
import { trace } from "../conf";
import chalk from "chalk";
function showError(error: any, message: any="[Error]: ") {
console.error(chalk.red(message), trace ? error : error.message);
}
export default showError;

View File

@ -1,30 +1,22 @@
import chalk from "chalk";
import { useContext, useEffect } from "react"; import { useContext, useEffect } from "react";
import { CredentialKeys } from "../app"; import { CredentialKeys } from "../app";
import authContext from "../context/authContext"; import authContext from "../context/authContext";
import spotifyApi from "../initializations/spotifyApi"; import spotifyApi from "../initializations/spotifyApi";
function useSpotifyApi() { function useSpotifyApi() {
const { access_token, clientId, clientSecret, expires_in, isLoggedIn, setExpires_in, setAccess_token } = useContext(authContext); const { access_token, clientId, clientSecret, isLoggedIn } = useContext(authContext);
const refreshToken = localStorage.getItem(CredentialKeys.refresh_token); const refreshToken = localStorage.getItem(CredentialKeys.refresh_token);
useEffect(() => { useEffect(() => {
if (isLoggedIn && clientId && clientSecret) { if (isLoggedIn && clientId && clientSecret && refreshToken) {
console.log(chalk.bgCyan.black("Setting up spotify credentials"))
spotifyApi.setClientId(clientId); spotifyApi.setClientId(clientId);
spotifyApi.setClientSecret(clientSecret); spotifyApi.setClientSecret(clientSecret);
spotifyApi.setRefreshToken(refreshToken);
spotifyApi.setAccessToken(access_token); spotifyApi.setAccessToken(access_token);
} }
const isExpiredToken = Date.now() > expires_in; }, [access_token, clientId, clientSecret, isLoggedIn]);
if (isLoggedIn && isExpiredToken && refreshToken) {
spotifyApi.setRefreshToken(refreshToken);
spotifyApi
.refreshAccessToken()
.then(({ body: { access_token, expires_in } }) => {
setAccess_token(access_token);
setExpires_in(expires_in);
})
.catch();
}
}, [access_token, clientId, clientSecret]);
return spotifyApi; return spotifyApi;
} }

View File

@ -0,0 +1,23 @@
import chalk from "chalk";
import { useContext } from "react";
import SpotifyWebApi from "spotify-web-api-node";
import authContext from "../context/authContext";
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)) {
try {
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]: ")
}
}
};
}
export default useSpotifyApiError;

View File

@ -0,0 +1,29 @@
import { useEffect } from "react";
import { QueryFunction, QueryKey, useQuery, UseQueryOptions, UseQueryResult } from "react-query";
import SpotifyWebApi from "spotify-web-api-node";
import useSpotifyApi from "./useSpotifyApi";
import useSpotifyApiError from "./useSpotifyApiError";
type SpotifyQueryFn<TQueryData> = (spotifyApi: SpotifyWebApi) => Promise<TQueryData>;
function useSpotifyQuery<TQueryData = unknown>(
queryKey: QueryKey,
queryHandler: SpotifyQueryFn<TQueryData>,
options: UseQueryOptions<TQueryData, SpotifyApi.ErrorObject> = {}
): UseQueryResult<TQueryData, SpotifyApi.ErrorObject> {
const spotifyApi = useSpotifyApi();
const handleSpotifyError = useSpotifyApiError(spotifyApi);
const query = useQuery<TQueryData, SpotifyApi.ErrorObject>(queryKey, ()=>queryHandler(spotifyApi), options);
const { isError, error } = query;
useEffect(() => {
if (isError && error) {
handleSpotifyError(error);
}
}, [isError, error]);
return query;
}
export default useSpotifyQuery;

View File

@ -15,8 +15,8 @@ function Routes() {
<Route exact path="/"> <Route exact path="/">
{isLoggedIn ? <Home /> : <Login />} {isLoggedIn ? <Home /> : <Login />}
</Route> </Route>
<Route path="/playlist/:id"><PlaylistView/></Route> <Route exact path="/playlist/:id"><PlaylistView/></Route>
<Route path="/genre/playlists/:id"><PlaylistGenreView/></Route> <Route exact path="/genre/playlists/:id"><PlaylistGenreView/></Route>
</> </>
); );
} }