mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
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:
parent
5a0486bc4f
commit
faea24e86e
12
.vscode/tasks.json
vendored
Normal file
12
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"problemMatcher": [],
|
||||
"label": "npm: start",
|
||||
"detail": "qode ./dist/index.js"
|
||||
}
|
||||
]
|
||||
}
|
8216
package-lock.json
generated
8216
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@
|
||||
"build": "webpack --mode=production",
|
||||
"dev": "TSC_WATCHFILE=UseFsEvents webpack --mode=development",
|
||||
"start": "qode ./dist/index.js",
|
||||
"start:trace": "qode ./dist/index.js --trace",
|
||||
"debug": "qode --inspect ./dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -16,6 +17,7 @@
|
||||
"@nodegui/react-nodegui": "^0.10.0",
|
||||
"axios": "^0.21.1",
|
||||
"base64url": "^3.0.1",
|
||||
"chalk": "^4.1.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"du": "^1.0.0",
|
||||
"express": "^4.17.1",
|
||||
@ -25,9 +27,12 @@
|
||||
"node-mpv": "^2.0.0-beta.1",
|
||||
"open": "^7.4.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-query": "^3.12.0",
|
||||
"react-router": "^5.2.0",
|
||||
"scrape-yt": "^1.4.7",
|
||||
"spotify-web-api-node": "^5.0.2"
|
||||
"spotify-web-api-node": "^5.0.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
@ -43,6 +48,7 @@
|
||||
"@types/react": "^16.9.49",
|
||||
"@types/react-router": "^5.1.11",
|
||||
"@types/spotify-web-api-node": "^5.0.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/webpack-env": "^1.15.3",
|
||||
"babel-loader": "^8.1.0",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
|
26
src/app.tsx
26
src/app.tsx
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Window, hot, View, useEventHandler } from "@nodegui/react-nodegui";
|
||||
import { QIcon, QKeyEvent, QMainWindow, QMainWindowSignals, WidgetEventTypes, WindowState } from "@nodegui/nodegui";
|
||||
import { Window, hot, View, useEventHandler, BoxView } from "@nodegui/react-nodegui";
|
||||
import { Direction, QIcon, QKeyEvent, QMainWindow, QMainWindowSignals, WidgetEventTypes, WindowState } from "@nodegui/nodegui";
|
||||
import nodeguiIcon from "../assets/nodegui.jpg";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import Routes from "./routes";
|
||||
@ -8,9 +8,11 @@ import { LocalStorage } from "node-localstorage";
|
||||
import authContext from "./context/authContext";
|
||||
import playerContext, { CurrentPlaylist, CurrentTrack } from "./context/playerContext";
|
||||
import Player, { audioPlayer } from "./components/Player";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import express from "express";
|
||||
import open from "open";
|
||||
import spotifyApi from "./initializations/spotifyApi";
|
||||
import showError from "./helpers/showError";
|
||||
|
||||
export enum CredentialKeys {
|
||||
credentials = "credentials",
|
||||
@ -25,6 +27,15 @@ export interface Credentials {
|
||||
const minSize = { width: 700, height: 750 };
|
||||
const winIcon = new QIcon(nodeguiIcon);
|
||||
global.localStorage = new LocalStorage("./local");
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
onError(error) {
|
||||
showError(error);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function RootApp() {
|
||||
const windowRef = useRef<QMainWindow>();
|
||||
@ -66,10 +77,9 @@ function RootApp() {
|
||||
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);
|
||||
const setExpireTime = (expirationDuration: number) => setExpires_in(Date.now() + expirationDuration * 1000 /* 1s = 1000 ms */);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoggedIn(!!cachedCredentials);
|
||||
@ -128,10 +138,14 @@ function RootApp() {
|
||||
<MemoryRouter>
|
||||
<authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, expires_in, setAccess_token, setExpires_in: setExpireTime, ...credentials }}>
|
||||
<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}>
|
||||
{/* <View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}> */}
|
||||
<BoxView direction={Direction.TopToBottom}>
|
||||
<Routes />
|
||||
{isLoggedIn && <Player />}
|
||||
</View>
|
||||
</BoxView>
|
||||
{/* </View> */}
|
||||
</QueryClientProvider>
|
||||
</playerContext.Provider>
|
||||
</authContext.Provider>
|
||||
</MemoryRouter>
|
||||
|
@ -1,39 +1,38 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Button, View, ScrollArea } from "@nodegui/react-nodegui";
|
||||
import playerContext from "../context/playerContext";
|
||||
import authContext from "../context/authContext";
|
||||
import { Button, ScrollArea, BoxView } from "@nodegui/react-nodegui";
|
||||
import { useHistory } from "react-router";
|
||||
import CachedImage from "./shared/CachedImage";
|
||||
import { CursorShape } from "@nodegui/nodegui";
|
||||
import { CursorShape, Direction } from "@nodegui/nodegui";
|
||||
import useSpotifyApi from "../hooks/useSpotifyApi";
|
||||
import showError from "../helpers/showError";
|
||||
import authContext from "../context/authContext";
|
||||
import useSpotifyApiError from "../hooks/useSpotifyApiError";
|
||||
|
||||
function Home() {
|
||||
const { currentPlaylist } = useContext(playerContext);
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const { access_token } = useContext(authContext);
|
||||
const [categories, setCategories] = useState<SpotifyApi.CategoryObject[]>([]);
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
|
||||
useEffect(() => {
|
||||
if (access_token) {
|
||||
(async () => {
|
||||
try {
|
||||
const categoriesReceived = await spotifyApi.getCategories({ country: "US" });
|
||||
setCategories(categoriesReceived.body.categories.items);
|
||||
} catch (error) {
|
||||
console.error("Spotify featured playlist loading failed: ", error);
|
||||
}
|
||||
})();
|
||||
if (categories.length === 0) {
|
||||
spotifyApi
|
||||
.getCategories({ country: "US" })
|
||||
.then((categoriesReceived) => setCategories(categoriesReceived.body.categories.items))
|
||||
.catch((error) => {
|
||||
showError(error, "[Spotify genre loading failed]: ");
|
||||
handleSpotifyError(error);
|
||||
});
|
||||
}
|
||||
}, [access_token]);
|
||||
|
||||
return (
|
||||
<ScrollArea style={`flex-grow: 1; border: none; flex: 1;`}>
|
||||
<View style={`flex-direction: 'column'; justify-content: 'center'; flex: 1;`}>
|
||||
{currentPlaylist && <CategoryCard key={((Math.random() * Date.now()) / Math.random()) * 100} id="current" name="Currently Playing" />}
|
||||
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
||||
<BoxView direction={Direction.TopToBottom}>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@ -45,39 +44,42 @@ interface CategoryCardProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
function CategoryCard({ id, name }: CategoryCardProps) {
|
||||
const CategoryCard = ({ id, name }: CategoryCardProps) => {
|
||||
const history = useHistory();
|
||||
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
|
||||
const { currentPlaylist } = useContext(playerContext);
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
if (id !== "current") {
|
||||
const playlistsRes = await spotifyApi.getPlaylistsForCategory(id, { limit: 4 });
|
||||
mounted && setPlaylists(playlistsRes.body.playlists.items);
|
||||
if (playlists.length === 0) {
|
||||
spotifyApi
|
||||
.getPlaylistsForCategory(id, { limit: 4 })
|
||||
.then((playlistsRes) => setPlaylists(playlistsRes.body.playlists.items))
|
||||
.catch((error) => {
|
||||
showError(error, `[Failed to get playlists of category ${name}]: `);
|
||||
handleSpotifyError(error);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to get playlists of category ${name} for: `, error);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
function goToGenre() {
|
||||
history.push(`/genre/playlists/${id}`, { name });
|
||||
}
|
||||
|
||||
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{
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: 'center';
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
#anchor-heading{
|
||||
@ -87,41 +89,22 @@ function CategoryCard({ id, name }: CategoryCardProps) {
|
||||
outline: none;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
align-self: 'flex-start';
|
||||
text-align: left;
|
||||
}
|
||||
#anchor-heading:hover{
|
||||
border: none;
|
||||
outline: none;
|
||||
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 {
|
||||
thumbnail: string;
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function PlaylistCard({ id, name, thumbnail }: PlaylistCardProps) {
|
||||
export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardProps) => {
|
||||
const history = useHistory();
|
||||
|
||||
function gotoPlaylist() {
|
||||
@ -130,9 +113,9 @@ export function PlaylistCard({ id, name, thumbnail }: PlaylistCardProps) {
|
||||
|
||||
const playlistStyleSheet = `
|
||||
#playlist-container{
|
||||
max-width: 250px;
|
||||
flex-direction: column;
|
||||
padding: 2px;
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
min-height: 150px;
|
||||
}
|
||||
#playlist-container:hover{
|
||||
border: 1px solid green;
|
||||
@ -143,8 +126,8 @@ export function PlaylistCard({ id, name, thumbnail }: PlaylistCardProps) {
|
||||
`;
|
||||
|
||||
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} />
|
||||
</View>
|
||||
</BoxView>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegul
|
||||
import IconButton from "./shared/IconButton";
|
||||
import authContext from "../context/authContext";
|
||||
import useSpotifyApi from "../hooks/useSpotifyApi";
|
||||
import showError from "../helpers/showError";
|
||||
|
||||
export const audioPlayer = new NodeMpv(
|
||||
{
|
||||
@ -59,6 +60,7 @@ function Player(): ReactElement {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to start audio player", error);
|
||||
showError(error, "[Failed starting audio player]: ")
|
||||
}
|
||||
})();
|
||||
|
||||
@ -98,7 +100,7 @@ function Player(): ReactElement {
|
||||
setIsStopped(true);
|
||||
setIsPaused(true);
|
||||
}
|
||||
console.error(error);
|
||||
showError(error, "[Failure at track change]: ");
|
||||
}
|
||||
})();
|
||||
}, [currentTrack]);
|
||||
@ -170,14 +172,13 @@ function Player(): ReactElement {
|
||||
setIsPaused(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showError(error, "[Track control failed]: ");
|
||||
}
|
||||
};
|
||||
|
||||
const prevOrNext = (constant: number) => {
|
||||
if (currentTrack && playlistTracksIds && currentPlaylist) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
@ -188,7 +189,7 @@ function Player(): ReactElement {
|
||||
await audioPlayer.stop();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to stop the audio player: ", error);
|
||||
showError(error, "[Failed at audio-player stop]: ")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,9 @@ import { ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useLocation, useParams } from "react-router";
|
||||
import authContext from "../context/authContext";
|
||||
import showError from "../helpers/showError";
|
||||
import useSpotifyApi from "../hooks/useSpotifyApi";
|
||||
import useSpotifyApiError from "../hooks/useSpotifyApiError";
|
||||
import BackButton from "./BackButton";
|
||||
import { PlaylistCard } from "./Home";
|
||||
|
||||
@ -12,23 +14,18 @@ function PlaylistGenreView() {
|
||||
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
|
||||
const { access_token, isLoggedIn } = useContext(authContext);
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
if (access_token) {
|
||||
const playlistsRes = await spotifyApi.getPlaylistsForCategory(id);
|
||||
mounted && setPlaylists(playlistsRes.body.playlists.items);
|
||||
if (playlists.length === 0 && access_token) {
|
||||
spotifyApi
|
||||
.getPlaylistsForCategory(id)
|
||||
.then((playlistsRes) => setPlaylists(playlistsRes.body.playlists.items))
|
||||
.catch((error) => {
|
||||
showError(error, `[Failed to get playlists of category ${location.state.name} for]: `);
|
||||
handleSpotifyError(error);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to get playlists of category ${name} for: `, error);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [access_token]);
|
||||
|
||||
const playlistGenreViewStylesheet = `
|
||||
|
@ -1,15 +1,15 @@
|
||||
import React, { FC, useContext, useEffect, useState } from "react";
|
||||
import { Button, ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||
import React, { FC, useContext } from "react";
|
||||
import { BoxView, Button, ScrollArea, Text } from "@nodegui/react-nodegui";
|
||||
import BackButton from "./BackButton";
|
||||
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 authContext from "../context/authContext";
|
||||
import playerContext from "../context/playerContext";
|
||||
import IconButton from "./shared/IconButton";
|
||||
import { heartRegular, play, stop } from "../icons";
|
||||
import { audioPlayer } from "./Player";
|
||||
import useSpotifyApi from "../hooks/useSpotifyApi";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||
|
||||
export interface PlaylistTrackRes {
|
||||
name: string;
|
||||
@ -18,28 +18,18 @@ export interface PlaylistTrackRes {
|
||||
}
|
||||
|
||||
const PlaylistView: FC = () => {
|
||||
const { isLoggedIn } = useContext(authContext);
|
||||
const { setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext);
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const params = useParams<{ id: string }>();
|
||||
const location = useLocation<{ name: string; thumbnail: string }>();
|
||||
const [tracks, setTracks] = useState<SpotifyApi.PlaylistTrackObject[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
(async () => {
|
||||
try {
|
||||
const tracks = await spotifyApi.getPlaylistTracks(params.id);
|
||||
setTracks(tracks.body.items);
|
||||
} catch (error) {
|
||||
console.error(`Failed to get tracks from ${params.id}: `, error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, []);
|
||||
const { data: tracks, isSuccess, isError, isLoading, refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>(
|
||||
[QueryCacheKeys.playlistTracks, params.id],
|
||||
(spotifyApi) => spotifyApi.getPlaylistTracks(params.id).then((track) => track.body.items),
|
||||
{ initialData: [] }
|
||||
);
|
||||
|
||||
const handlePlaylistPlayPause = () => {
|
||||
if (currentPlaylist?.id !== params.id) {
|
||||
if (currentPlaylist?.id !== params.id && isSuccess && tracks) {
|
||||
setCurrentPlaylist({ ...params, ...location.state, tracks });
|
||||
setCurrentTrack(tracks[0].track);
|
||||
} else {
|
||||
@ -49,32 +39,38 @@ const PlaylistView: FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const trackClickHandler = async (track: SpotifyApi.TrackObjectFull) => {
|
||||
try {
|
||||
const trackClickHandler = (track: SpotifyApi.TrackObjectFull) => {
|
||||
setCurrentTrack(track);
|
||||
} catch (error) {
|
||||
console.error("Failed to resolve track's youtube url: ", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={`flex-direction: 'column'; flex-grow: 1;`}>
|
||||
<View style={`justify-content: 'space-between'; padding-bottom: 10px; padding-left: 10px;`}>
|
||||
<BoxView direction={Direction.TopToBottom}>
|
||||
<BoxView style={`max-width: 150px;`}>
|
||||
<BackButton />
|
||||
<View style={`height: 50px; justify-content: 'space-between'; width: 100px; padding-right: 20px;`}>
|
||||
<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>
|
||||
</BoxView>
|
||||
<Text>{`<center><h2>${location.state.name[0].toUpperCase()}${location.state.name.slice(1)}</h2></center>`}</Text>
|
||||
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
||||
<View style={`flex-direction:column;`}>
|
||||
{isLoggedIn &&
|
||||
tracks.length > 0 &&
|
||||
tracks.map(({ track }, index) => {
|
||||
<BoxView /* style={`flex-direction:column;`} */ direction={Direction.TopToBottom}>
|
||||
{isLoading && <Text>{`Loading Tracks...`}</Text>}
|
||||
{isError && (
|
||||
<>
|
||||
<Text>{`Failed to load ${location.state.name} tracks`}</Text>
|
||||
<Button
|
||||
on={{
|
||||
clicked() {
|
||||
refetch();
|
||||
},
|
||||
}}
|
||||
text="Retry"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{tracks?.map(({ track }, index) => {
|
||||
return (
|
||||
<TrackButton
|
||||
key={index * ((Date.now() / Math.random()) * 100)}
|
||||
key={index+track.id}
|
||||
active={currentTrack?.id === track.id && currentPlaylist?.id === params.id}
|
||||
artist={track.artists.map((x) => x.name).join(", ")}
|
||||
name={track.name}
|
||||
@ -82,9 +78,9 @@ const PlaylistView: FC = () => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</BoxView>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
</BoxView>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { Image, Text, View } from "@nodegui/react-nodegui";
|
||||
import { QLabel } from "@nodegui/nodegui";
|
||||
import { ImageProps } from "@nodegui/react-nodegui/dist/components/Image/RNImage";
|
||||
import { getCachedImageBuffer } from "../../helpers/getCachedImageBuffer";
|
||||
import showError from "../../helpers/showError";
|
||||
|
||||
interface CachedImageProps extends Omit<ImageProps, "buffer"> {
|
||||
src: string;
|
||||
@ -15,18 +16,17 @@ function CachedImage({ src, alt, ...props }: CachedImageProps) {
|
||||
const [imageProcessError, setImageProcessError] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
mounted && setImageBuffer(await getCachedImageBuffer(src, props.maxSize ?? props.size));
|
||||
} catch (error) {
|
||||
mounted && setImageProcessError(false);
|
||||
console.log("Cached Image Error:", error);
|
||||
if (imageBuffer===undefined) {
|
||||
getCachedImageBuffer(src, props.maxSize ?? props.size)
|
||||
.then((buffer) => setImageBuffer(buffer))
|
||||
.catch((error) => {
|
||||
setImageProcessError(false);
|
||||
showError(error, "[Cached Image Error]: ");
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
imgRef.current?.close();
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
return !imageProcessError && imageBuffer ? (
|
||||
|
@ -3,4 +3,11 @@ import { join } from "path";
|
||||
|
||||
const env = dotenv.config({path: join(process.cwd(), ".env")}).parsed as any
|
||||
export const clientId = "";
|
||||
export const trace = process.argv.find(arg => arg === "--trace") ?? false;
|
||||
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
8
src/helpers/showError.ts
Normal 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;
|
@ -1,30 +1,22 @@
|
||||
import chalk from "chalk";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { CredentialKeys } from "../app";
|
||||
import authContext from "../context/authContext";
|
||||
import spotifyApi from "../initializations/spotifyApi";
|
||||
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn && clientId && clientSecret) {
|
||||
if (isLoggedIn && clientId && clientSecret && refreshToken) {
|
||||
console.log(chalk.bgCyan.black("Setting up spotify credentials"))
|
||||
spotifyApi.setClientId(clientId);
|
||||
spotifyApi.setClientSecret(clientSecret);
|
||||
spotifyApi.setRefreshToken(refreshToken);
|
||||
spotifyApi.setAccessToken(access_token);
|
||||
}
|
||||
const isExpiredToken = Date.now() > expires_in;
|
||||
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]);
|
||||
}, [access_token, clientId, clientSecret, isLoggedIn]);
|
||||
|
||||
return spotifyApi;
|
||||
}
|
||||
|
23
src/hooks/useSpotifyApiError.ts
Normal file
23
src/hooks/useSpotifyApiError.ts
Normal 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;
|
29
src/hooks/useSpotifyQuery.ts
Normal file
29
src/hooks/useSpotifyQuery.ts
Normal 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;
|
@ -15,8 +15,8 @@ function Routes() {
|
||||
<Route exact path="/">
|
||||
{isLoggedIn ? <Home /> : <Login />}
|
||||
</Route>
|
||||
<Route path="/playlist/:id"><PlaylistView/></Route>
|
||||
<Route path="/genre/playlists/:id"><PlaylistGenreView/></Route>
|
||||
<Route exact path="/playlist/:id"><PlaylistView/></Route>
|
||||
<Route exact path="/genre/playlists/:id"><PlaylistGenreView/></Route>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user