Fixed: <Switch> inifinite loop

Added: prefrencesContext
Edited: PlaylistCard for enabling images
This commit is contained in:
KRTirtho 2021-03-30 12:13:41 +06:00
parent 09467e6e9a
commit b0c6781df4
7 changed files with 160 additions and 92 deletions

View File

@ -12,14 +12,16 @@ 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"; import showError from "./helpers/showError";
import fs from "fs" import fs from "fs";
import path from "path"; import path from "path";
import { confDir } from "./conf"; import { confDir } from "./conf";
import spotubeIcon from "../assets/icon.svg"; import spotubeIcon from "../assets/icon.svg";
import preferencesContext, { PreferencesContextProperties } from "./context/preferencesContext";
export enum CredentialKeys { export enum LocalStorageKeys {
credentials = "credentials", credentials = "credentials",
refresh_token = "refresh_token", refresh_token = "refresh_token",
preferences = "user-preferences",
} }
export interface Credentials { export interface Credentials {
@ -30,7 +32,7 @@ export interface Credentials {
const minSize = { width: 700, height: 750 }; const minSize = { width: 700, height: 750 };
const winIcon = new QIcon(spotubeIcon); const winIcon = new QIcon(spotubeIcon);
const localStorageDir = path.join(confDir, "local"); const localStorageDir = path.join(confDir, "local");
fs.mkdirSync(localStorageDir, {recursive: true}); fs.mkdirSync(localStorageDir, { recursive: true });
global.localStorage = new LocalStorage(localStorageDir); global.localStorage = new LocalStorage(localStorageDir);
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@ -42,6 +44,11 @@ const queryClient = new QueryClient({
}, },
}); });
const initialPreferences: PreferencesContextProperties = {
playlistImages: false,
};
//* Application start
function RootApp() { function RootApp() {
const windowRef = useRef<QMainWindow>(); const windowRef = useRef<QMainWindow>();
const [currentTrack, setCurrentTrack] = useState<CurrentTrack>(); const [currentTrack, setCurrentTrack] = useState<CurrentTrack>();
@ -75,16 +82,28 @@ function RootApp() {
}, },
[currentTrack] [currentTrack]
); );
// cache
const cachedPreferences = localStorage.getItem(LocalStorageKeys.preferences);
const cachedCredentials = localStorage.getItem(LocalStorageKeys.credentials);
// state
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false); const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
const [credentials, setCredentials] = useState<Credentials>({ clientId: "", clientSecret: "" }); const [credentials, setCredentials] = useState<Credentials>({ clientId: "", clientSecret: "" });
const [preferences, setPreferences] = useState<PreferencesContextProperties>(() => {
if (cachedPreferences) {
return JSON.parse(cachedPreferences);
}
return initialPreferences;
});
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);
useEffect(() => { useEffect(() => {
setIsLoggedIn(!!cachedCredentials); setIsLoggedIn(!!cachedCredentials);
}, []); }, []);
// just saves the preferences
useEffect(() => {
localStorage.setItem(LocalStorageKeys.preferences, JSON.stringify(preferences));
}, [preferences]);
useEffect(() => { useEffect(() => {
const onWindowClose = () => { const onWindowClose = () => {
@ -100,7 +119,7 @@ function RootApp() {
}); });
// for user code login // for user code login
useEffect(() => { useEffect(() => {
if (isLoggedIn && credentials && !localStorage.getItem(CredentialKeys.refresh_token)) { if (isLoggedIn && credentials && !localStorage.getItem(LocalStorageKeys.refresh_token)) {
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
@ -110,7 +129,7 @@ function RootApp() {
spotifyApi.setClientSecret(credentials.clientSecret); spotifyApi.setClientSecret(credentials.clientSecret);
const { body: authRes } = await spotifyApi.authorizationCodeGrant(req.query.code); const { body: authRes } = await spotifyApi.authorizationCodeGrant(req.query.code);
setAccess_token(authRes.access_token); setAccess_token(authRes.access_token);
localStorage.setItem(CredentialKeys.refresh_token, authRes.refresh_token); localStorage.setItem(LocalStorageKeys.refresh_token, authRes.refresh_token);
return res.end(); return res.end();
} catch (error) { } catch (error) {
console.error("Failed to fullfil code grant flow: ", error); console.error("Failed to fullfil code grant flow: ", error);
@ -121,7 +140,7 @@ function RootApp() {
console.log("Server is running"); console.log("Server is running");
spotifyApi.setClientId(credentials.clientId); spotifyApi.setClientId(credentials.clientId);
spotifyApi.setClientSecret(credentials.clientSecret); spotifyApi.setClientSecret(credentials.clientSecret);
open(spotifyApi.createAuthorizeURL(["user-library-read", "playlist-read-private", "user-library-modify","playlist-modify-private", "playlist-modify-public"], "xxxyyysssddd")).catch((e) => open(spotifyApi.createAuthorizeURL(["user-library-read", "playlist-read-private", "user-library-modify", "playlist-modify-private", "playlist-modify-public"], "xxxyyysssddd")).catch((e) =>
console.error("Opening IPC connection with browser failed: ", e) console.error("Opening IPC connection with browser failed: ", e)
); );
}); });
@ -141,14 +160,16 @@ function RootApp() {
<Window ref={windowRef} on={windowEvents} windowState={WindowState.WindowMaximized} windowIcon={winIcon} windowTitle="Spotube" minSize={minSize}> <Window ref={windowRef} on={windowEvents} windowState={WindowState.WindowMaximized} windowIcon={winIcon} windowTitle="Spotube" minSize={minSize}>
<MemoryRouter> <MemoryRouter>
<authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, setAccess_token, ...credentials }}> <authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, setAccess_token, ...credentials }}>
<playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}> <preferencesContext.Provider value={{ ...preferences, setPreferences }}>
<QueryClientProvider client={queryClient}> <playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}>
<View style={`flex: 1; flex-direction: 'column'; align-items: 'stretch';`}> <QueryClientProvider client={queryClient}>
<Routes /> <View style={`flex: 1; flex-direction: 'column'; align-items: 'stretch';`}>
{isLoggedIn && <Player />} <Routes />
</View> {isLoggedIn && <Player />}
</QueryClientProvider> </View>
</playerContext.Provider> </QueryClientProvider>
</playerContext.Provider>
</preferencesContext.Provider>
</authContext.Provider> </authContext.Provider>
</MemoryRouter> </MemoryRouter>
</Window> </Window>

View File

@ -1,7 +1,7 @@
import React, { useContext, useState } from "react"; import React, { useContext, useState } from "react";
import { LineEdit, Text, Button, View } from "@nodegui/react-nodegui"; import { LineEdit, Text, Button, View } from "@nodegui/react-nodegui";
import authContext from "../context/authContext"; import authContext from "../context/authContext";
import { CredentialKeys, Credentials } from "../app"; import { LocalStorageKeys, Credentials } from "../app";
function Login() { function Login() {
const { setIsLoggedIn } = useContext(authContext); const { setIsLoggedIn } = useContext(authContext);
@ -55,7 +55,7 @@ function Login() {
on={{ on={{
clicked: () => { clicked: () => {
localStorage.setItem( localStorage.setItem(
CredentialKeys.credentials, LocalStorageKeys.credentials,
JSON.stringify({ JSON.stringify({
clientId: credentials.clientId, clientId: credentials.clientId,
clientSecret: credentials.clientSecret, clientSecret: credentials.clientSecret,

View File

@ -1,14 +1,20 @@
import { Text, View } from "@nodegui/react-nodegui"; import { Text, View } from "@nodegui/react-nodegui";
import React from "react"; import React, { useContext } from "react";
import Switch from "./shared/Switch"; import preferencesContext from "../context/preferencesContext";
import Switch, { SwitchProps } from "./shared/Switch";
function Settings() { function Settings() {
const { setPreferences, ...preferences } = useContext(preferencesContext)
return ( return (
<View style="flex: 1; flex-direction: 'column'; justify-content: 'flex-start';"> <View style="flex: 1; flex-direction: 'column'; justify-content: 'flex-start';">
<Text>{`<center><h2>Settings</h2></center>`}</Text> <Text>{`<center><h2>Settings</h2></center>`}</Text>
<View style="width: '100%'; flex-direction: 'column'; justify-content: 'flex-start';"> <View style="width: '100%'; flex-direction: 'column'; justify-content: 'flex-start';">
<SettingsCheckTile title="Use images instead of colors for playlist" subtitle="This will increase memory usage" /> <SettingsCheckTile
<SettingsCheckTile title="Some unknown settings" /> checked={preferences.playlistImages}
title="Use images instead of colors for playlist"
subtitle="This will increase memory usage"
onChange={(checked) => setPreferences({ ...preferences, playlistImages: checked })}
/>
</View> </View>
</View> </View>
); );
@ -19,9 +25,11 @@ export default Settings;
interface SettingsCheckTileProps { interface SettingsCheckTileProps {
title: string; title: string;
subtitle?: string; subtitle?: string;
checked: boolean;
onChange?: SwitchProps["onChange"];
} }
export function SettingsCheckTile({ title, subtitle = "" }: SettingsCheckTileProps) { export function SettingsCheckTile({ title, subtitle = "", onChange, checked }: SettingsCheckTileProps) {
return ( return (
<View style="flex: 1; align-items: 'center'; padding: 15px 0; justify-content: 'space-between';"> <View style="flex: 1; align-items: 'center'; padding: 15px 0; justify-content: 'space-between';">
<Text> <Text>
@ -30,7 +38,7 @@ export function SettingsCheckTile({ title, subtitle = "" }: SettingsCheckTilePro
<p>${subtitle}</p> <p>${subtitle}</p>
`} `}
</Text> </Text>
<Switch checked/> <Switch checked={checked} onChange={onChange} />
</View> </View>
); );
} }

View File

@ -1,22 +1,26 @@
import { CursorShape, QIcon, QMouseEvent } from '@nodegui/nodegui'; import { CursorShape, QIcon, QMouseEvent } from "@nodegui/nodegui";
import { Text, View } from '@nodegui/react-nodegui'; import { Text, View } from "@nodegui/react-nodegui";
import React, { useContext, useMemo, useState } from 'react' import React, { useContext, useMemo, useState } from "react";
import { useHistory } from 'react-router'; import { useHistory } from "react-router";
import { QueryCacheKeys } from '../../conf'; import { QueryCacheKeys } from "../../conf";
import playerContext from '../../context/playerContext'; import playerContext from "../../context/playerContext";
import { generateRandomColor, getDarkenForeground } from '../../helpers/RandomColor'; import preferencesContext from "../../context/preferencesContext";
import showError from '../../helpers/showError'; import { generateRandomColor, getDarkenForeground } from "../../helpers/RandomColor";
import usePlaylistReaction from '../../hooks/usePlaylistReaction'; import showError from "../../helpers/showError";
import useSpotifyQuery from '../../hooks/useSpotifyQuery'; import usePlaylistReaction from "../../hooks/usePlaylistReaction";
import { heart, heartRegular, pause, play } from '../../icons'; import useSpotifyQuery from "../../hooks/useSpotifyQuery";
import { audioPlayer } from '../Player'; import { heart, heartRegular, pause, play } from "../../icons";
import IconButton from './IconButton'; import { audioPlayer } from "../Player";
import CachedImage from "./CachedImage";
import IconButton from "./IconButton";
interface PlaylistCardProps { interface PlaylistCardProps {
playlist: SpotifyApi.PlaylistObjectSimplified; playlist: SpotifyApi.PlaylistObjectSimplified;
} }
const PlaylistCard = ({ playlist }: PlaylistCardProps) => { const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
const preferences = useContext(preferencesContext);
const thumbnail = playlist.images[0].url;
const { id, description, name, images } = playlist; const { id, description, name, images } = playlist;
const history = useHistory(); const history = useHistory();
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
@ -31,7 +35,7 @@ const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
try { try {
const { data: tracks, isSuccess } = await refetch(); const { data: tracks, isSuccess } = await refetch();
if (currentPlaylist?.id !== id && isSuccess && tracks) { if (currentPlaylist?.id !== id && isSuccess && tracks) {
setCurrentPlaylist({ tracks, id, name, thumbnail: images[0].url }); setCurrentPlaylist({ tracks, id, name, thumbnail });
setCurrentTrack(tracks[0].track); setCurrentTrack(tracks[0].track);
} else { } else {
await audioPlayer.stop(); await audioPlayer.stop();
@ -46,7 +50,7 @@ const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
function gotoPlaylist(native?: any) { function gotoPlaylist(native?: any) {
const key = new QMouseEvent(native); const key = new QMouseEvent(native);
if (key.button() === 1) { if (key.button() === 1) {
history.push(`/playlist/${id}`, { name, thumbnail: images[0].url }); history.push(`/playlist/${id}`, { name, thumbnail });
} }
} }
@ -54,14 +58,16 @@ const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
const color = useMemo(() => getDarkenForeground(bgColor1), [bgColor1]); const color = useMemo(() => getDarkenForeground(bgColor1), [bgColor1]);
const playlistStyleSheet = ` const playlistStyleSheet = `
#playlist-container{ #playlist-container, #img-container{
width: 150px; width: 150px;
flex-direction: column;
padding: 10px; padding: 10px;
min-height: 150px;
background-color: ${bgColor1};
border-radius: 5px;
margin: 5px; margin: 5px;
flex-direction: column;
background-color: ${bgColor1};
}
#playlist-container{
border-radius: 5px;
min-height: 150px;
} }
#playlist-container:hover{ #playlist-container:hover{
border: 1px solid green; border: 1px solid green;
@ -70,23 +76,55 @@ const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
border: 5px solid green; border: 5px solid green;
} }
`; `;
const playlistAction = `
position: absolute;
bottom: 30px;
background-color: ${color};
`;
const playlistActions = (
<>
<IconButton
style={preferences.playlistImages ? "" : playlistAction + "left: '55%'"}
icon={new QIcon(isFavorite(id) ? heart : heartRegular)}
on={{
clicked() {
reactToPlaylist(playlist);
},
}}
/>
<IconButton
style={preferences.playlistImages ? "" : playlistAction + "left: '80%'"}
icon={new QIcon(currentPlaylist?.id === id ? pause : play)}
on={{
clicked() {
handlePlaylistPlayPause();
},
}}
/>
</>
);
const hovers = {
HoverEnter() {
setHovered(true);
},
HoverLeave() {
setHovered(false);
},
};
return ( return (
<View <View
id="playlist-container" id={preferences.playlistImages ? "img-container" : "playlist-container"}
cursor={CursorShape.PointingHandCursor} cursor={CursorShape.PointingHandCursor}
styleSheet={playlistStyleSheet} styleSheet={playlistStyleSheet}
on={{ on={{
MouseButtonRelease: gotoPlaylist, MouseButtonRelease: gotoPlaylist,
HoverEnter() { ...hovers,
setHovered(true);
},
HoverLeave() {
setHovered(false);
},
}}> }}>
{/* <CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} /> */} {preferences.playlistImages && <CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} />}
<Text style={`color: ${color};`} wordWrap on={{ MouseButtonRelease: gotoPlaylist }}>
<Text style={`color: ${color};`} wordWrap on={{ MouseButtonRelease: gotoPlaylist, ...hovers }}>
{` {`
<center> <center>
<h3>${name}</h3> <h3>${name}</h3>
@ -94,28 +132,12 @@ const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
</center> </center>
`} `}
</Text> </Text>
{(hovered || currentPlaylist?.id === id) && (
<> {(hovered || currentPlaylist?.id === id) && !preferences.playlistImages && playlistActions}
<IconButton {preferences.playlistImages &&
style={`position: absolute; bottom: 30px; left: '55%'; background-color: ${color};`} <View style="flex: 1; justify-content: 'space-around';">{playlistActions}
icon={new QIcon(isFavorite(id) ? heart : heartRegular)} </View>
on={{ }
clicked() {
reactToPlaylist(playlist);
},
}}
/>
<IconButton
icon={new QIcon(currentPlaylist?.id === id ? pause : play)}
style={`position: absolute; bottom: 30px; left: '80%'; background-color: ${color};`}
on={{
clicked() {
handlePlaylistPlayPause();
},
}}
/>
</>
)}
</View> </View>
); );
}; };

View File

@ -3,20 +3,22 @@ import { Slider } from "@nodegui/react-nodegui";
import { CheckBoxProps } from "@nodegui/react-nodegui/dist/components/CheckBox/RNCheckBox"; import { CheckBoxProps } from "@nodegui/react-nodegui/dist/components/CheckBox/RNCheckBox";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
interface SwitchProps extends Omit<CheckBoxProps, "on" |"icon" | "text">{ export interface SwitchProps extends Omit<CheckBoxProps, "on" | "icon" | "text"> {
onChange?(checked:boolean): void onChange?(checked: boolean): void;
} }
function Switch({ checked, onChange, ...props }: SwitchProps) { function Switch({ checked: derivedChecked, onChange, ...props }: SwitchProps) {
const [value, setValue] = useState<0|1>(0); const [checked, setChecked] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
setValue(checked ? 1 : 0); if (derivedChecked) {
}, [checked]) setChecked(derivedChecked);
}
}, []);
return ( return (
<Slider <Slider
value={value} value={checked ? 1 : 0}
hasTracking hasTracking
mouseTracking mouseTracking
orientation={Orientation.Horizontal} orientation={Orientation.Horizontal}
@ -25,18 +27,18 @@ function Switch({ checked, onChange, ...props }: SwitchProps) {
maxSize={{ width: 30, height: 20 }} maxSize={{ width: 30, height: 20 }}
on={{ on={{
valueChanged(value) { valueChanged(value) {
onChange && onChange(value === 1); onChange && onChange(value===1);
}, },
MouseButtonRelease(native: any) { MouseButtonRelease(native: any) {
const mouse = new QMouseEvent(native); const mouse = new QMouseEvent(native);
if (mouse.button() === 1) { if (mouse.button() === 1) {
setValue(value===1?0:1) setChecked(!checked);
} }
} },
} }}
}
{...props} {...props}
/>); />
);
} }
export default Switch; export default Switch;

View File

@ -0,0 +1,15 @@
import React, { Dispatch, SetStateAction } from "react";
export interface PreferencesContextProperties {
playlistImages: boolean;
}
export interface PreferencesContext extends PreferencesContextProperties {
setPreferences: Dispatch<SetStateAction<PreferencesContextProperties>>;
}
const preferencesContext = React.createContext<PreferencesContext>({
playlistImages: false,
setPreferences() { }
});
export default preferencesContext;

View File

@ -1,6 +1,6 @@
import chalk from "chalk"; import chalk from "chalk";
import { useContext, useEffect } from "react"; import { useContext, useEffect } from "react";
import { CredentialKeys } from "../app"; import { LocalStorageKeys } from "../app";
import authContext from "../context/authContext"; import authContext from "../context/authContext";
import showError from "../helpers/showError"; import showError from "../helpers/showError";
import spotifyApi from "../initializations/spotifyApi"; import spotifyApi from "../initializations/spotifyApi";
@ -13,7 +13,7 @@ function useSpotifyApi() {
isLoggedIn, isLoggedIn,
setAccess_token, setAccess_token,
} = useContext(authContext); } = useContext(authContext);
const refreshToken = localStorage.getItem(CredentialKeys.refresh_token); const refreshToken = localStorage.getItem(LocalStorageKeys.refresh_token);
useEffect(() => { useEffect(() => {
if (isLoggedIn && clientId && clientSecret && refreshToken) { if (isLoggedIn && clientId && clientSecret && refreshToken) {