mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
Fixed: <Switch> inifinite loop
Added: prefrencesContext Edited: PlaylistCard for enabling images
This commit is contained in:
parent
09467e6e9a
commit
b0c6781df4
53
src/app.tsx
53
src/app.tsx
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
15
src/context/preferencesContext.ts
Normal file
15
src/context/preferencesContext.ts
Normal 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;
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user