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

View File

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

View File

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

View File

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

View File

@ -3,20 +3,22 @@ import { Slider } from "@nodegui/react-nodegui";
import { CheckBoxProps } from "@nodegui/react-nodegui/dist/components/CheckBox/RNCheckBox";
import React, { useEffect, useState } from "react";
interface SwitchProps extends Omit<CheckBoxProps, "on" |"icon" | "text">{
onChange?(checked:boolean): void
export interface SwitchProps extends Omit<CheckBoxProps, "on" | "icon" | "text"> {
onChange?(checked: boolean): void;
}
function Switch({ checked, onChange, ...props }: SwitchProps) {
const [value, setValue] = useState<0|1>(0);
function Switch({ checked: derivedChecked, onChange, ...props }: SwitchProps) {
const [checked, setChecked] = useState<boolean>(false);
useEffect(() => {
setValue(checked ? 1 : 0);
}, [checked])
if (derivedChecked) {
setChecked(derivedChecked);
}
}, []);
return (
<Slider
value={value}
value={checked ? 1 : 0}
hasTracking
mouseTracking
orientation={Orientation.Horizontal}
@ -25,18 +27,18 @@ function Switch({ checked, onChange, ...props }: SwitchProps) {
maxSize={{ width: 30, height: 20 }}
on={{
valueChanged(value) {
onChange && onChange(value === 1);
onChange && onChange(value===1);
},
MouseButtonRelease(native: any) {
const mouse = new QMouseEvent(native);
if (mouse.button() === 1) {
setValue(value===1?0:1)
setChecked(!checked);
}
}
}
}
},
}}
{...props}
/>);
/>
);
}
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 { useContext, useEffect } from "react";
import { CredentialKeys } from "../app";
import { LocalStorageKeys } from "../app";
import authContext from "../context/authContext";
import showError from "../helpers/showError";
import spotifyApi from "../initializations/spotifyApi";
@ -13,7 +13,7 @@ function useSpotifyApi() {
isLoggedIn,
setAccess_token,
} = useContext(authContext);
const refreshToken = localStorage.getItem(CredentialKeys.refresh_token);
const refreshToken = localStorage.getItem(LocalStorageKeys.refresh_token);
useEffect(() => {
if (isLoggedIn && clientId && clientSecret && refreshToken) {