- restart the app to complete login
- no volume caching
- cached image memory leak
This commit is contained in:
KRTirtho 2021-03-30 22:14:15 +06:00
parent b0c6781df4
commit 4b513f91d8
5 changed files with 64 additions and 52 deletions

View File

@ -22,6 +22,7 @@ export enum LocalStorageKeys {
credentials = "credentials", credentials = "credentials",
refresh_token = "refresh_token", refresh_token = "refresh_token",
preferences = "user-preferences", preferences = "user-preferences",
volume = "volume"
} }
export interface Credentials { export interface Credentials {
@ -47,6 +48,7 @@ const queryClient = new QueryClient({
const initialPreferences: PreferencesContextProperties = { const initialPreferences: PreferencesContextProperties = {
playlistImages: false, playlistImages: false,
}; };
const initialCredentials: Credentials = { clientId: "", clientSecret: "" };
//* Application start //* Application start
function RootApp() { function RootApp() {
@ -87,7 +89,13 @@ function RootApp() {
const cachedCredentials = localStorage.getItem(LocalStorageKeys.credentials); const cachedCredentials = localStorage.getItem(LocalStorageKeys.credentials);
// state // 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>(() => {
if (cachedCredentials) {
return JSON.parse(cachedCredentials);
}
return initialCredentials;
});
const [preferences, setPreferences] = useState<PreferencesContextProperties>(() => { const [preferences, setPreferences] = useState<PreferencesContextProperties>(() => {
if (cachedPreferences) { if (cachedPreferences) {
return JSON.parse(cachedPreferences); return JSON.parse(cachedPreferences);
@ -98,28 +106,15 @@ function RootApp() {
const [currentPlaylist, setCurrentPlaylist] = useState<CurrentPlaylist>(); const [currentPlaylist, setCurrentPlaylist] = useState<CurrentPlaylist>();
useEffect(() => { useEffect(() => {
setIsLoggedIn(!!cachedCredentials); const parsedCredentials: Credentials = JSON.parse(cachedCredentials ?? "{}");
setIsLoggedIn(!!(parsedCredentials.clientId && parsedCredentials.clientSecret));
}, []); }, []);
// just saves the preferences
useEffect(() => {
localStorage.setItem(LocalStorageKeys.preferences, JSON.stringify(preferences));
}, [preferences]);
useEffect(() => {
const onWindowClose = () => {
if (audioPlayer.isRunning()) {
audioPlayer.stop().catch((e) => console.error("Failed to quit MPV player: ", e));
}
};
windowRef.current?.addEventListener(WidgetEventTypes.Close, onWindowClose);
return () => {
windowRef.current?.removeEventListener(WidgetEventTypes.Close, onWindowClose);
};
});
// for user code login // for user code login
useEffect(() => { useEffect(() => {
if (isLoggedIn && credentials && !localStorage.getItem(LocalStorageKeys.refresh_token)) { // saving changed credentials to storage
localStorage.setItem(LocalStorageKeys.credentials, JSON.stringify(credentials));
if (credentials.clientId && credentials.clientSecret && !localStorage.getItem(LocalStorageKeys.refresh_token)) {
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
@ -130,6 +125,7 @@ function RootApp() {
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(LocalStorageKeys.refresh_token, authRes.refresh_token); localStorage.setItem(LocalStorageKeys.refresh_token, authRes.refresh_token);
setIsLoggedIn(true);
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);
@ -148,18 +144,31 @@ function RootApp() {
server.close(() => console.log("Closed server")); server.close(() => console.log("Closed server"));
}; };
} }
}, [isLoggedIn, credentials]); }, [credentials]);
// just saves the preferences
useEffect(() => { useEffect(() => {
if (cachedCredentials) { localStorage.setItem(LocalStorageKeys.preferences, JSON.stringify(preferences));
setCredentials(JSON.parse(cachedCredentials)); }, [preferences]);
// window event listeners
useEffect(() => {
const onWindowClose = () => {
if (audioPlayer.isRunning()) {
audioPlayer.stop().catch((e) => console.error("Failed to quit MPV player: ", e));
} }
}, [isLoggedIn]); };
windowRef.current?.addEventListener(WidgetEventTypes.Close, onWindowClose);
return () => {
windowRef.current?.removeEventListener(WidgetEventTypes.Close, onWindowClose);
};
});
return ( return (
<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, setCredentials }}>
<preferencesContext.Provider value={{ ...preferences, setPreferences }}> <preferencesContext.Provider value={{ ...preferences, setPreferences }}>
<playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}> <playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>

View File

@ -1,10 +1,9 @@
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 { LocalStorageKeys, Credentials } from "../app";
function Login() { function Login() {
const { setIsLoggedIn } = useContext(authContext); const { setCredentials: setGlobalCredentials } = useContext(authContext);
const [credentials, setCredentials] = useState({ const [credentials, setCredentials] = useState({
clientId: "", clientId: "",
clientSecret: "", clientSecret: "",
@ -54,14 +53,7 @@ function Login() {
<Button <Button
on={{ on={{
clicked: () => { clicked: () => {
localStorage.setItem( setGlobalCredentials(credentials);
LocalStorageKeys.credentials,
JSON.stringify({
clientId: credentials.clientId,
clientSecret: credentials.clientSecret,
} as Credentials)
);
setIsLoggedIn(true);
}, },
}} }}
text="Add" text="Add"

View File

@ -11,6 +11,7 @@ import IconButton from "./shared/IconButton";
import showError from "../helpers/showError"; import showError from "../helpers/showError";
import useTrackReaction from "../hooks/useTrackReaction"; import useTrackReaction from "../hooks/useTrackReaction";
import ManualLyricDialog from "./ManualLyricDialog"; import ManualLyricDialog from "./ManualLyricDialog";
import { LocalStorageKeys } from "../app";
export const audioPlayer = new NodeMpv( export const audioPlayer = new NodeMpv(
{ {
@ -26,8 +27,9 @@ export const audioPlayer = new NodeMpv(
function Player(): ReactElement { function Player(): ReactElement {
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext); const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
const { reactToTrack, isFavorite } = useTrackReaction(); const { reactToTrack, isFavorite } = useTrackReaction();
const cachedVolume = localStorage.getItem(LocalStorageKeys.volume);
const [isPaused, setIsPaused] = useState(true); const [isPaused, setIsPaused] = useState(true);
const [volume, setVolume] = useState<number>(55); const [volume, setVolume] = useState<number>(() => (cachedVolume ? parseFloat(cachedVolume) : 55));
const [totalDuration, setTotalDuration] = useState<number>(0); const [totalDuration, setTotalDuration] = useState<number>(0);
const [shuffle, setShuffle] = useState<boolean>(false); const [shuffle, setShuffle] = useState<boolean>(false);
const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]); const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]);
@ -40,10 +42,10 @@ function Player(): ReactElement {
setVolume(value); setVolume(value);
}, },
sliderReleased: () => { sliderReleased: () => {
localStorage.setItem("volume", volume.toString()); localStorage.setItem(LocalStorageKeys.volume, volume.toString());
}, },
}, },
[] [volume]
); );
const playerRunning = audioPlayer.isRunning(); const playerRunning = audioPlayer.isRunning();
const titleRef = useRef<QLabel>(); const titleRef = useRef<QLabel>();
@ -223,11 +225,7 @@ function Player(): ReactElement {
}} }}
icon={new QIcon(isFavorite(currentTrack?.id ?? "") ? heart : heartRegular)} icon={new QIcon(isFavorite(currentTrack?.id ?? "") ? heart : heartRegular)}
/> />
<IconButton <IconButton style={openLyrics ? "background-color: green;" : ""} icon={new QIcon(musicNode)} on={{ clicked: () => currentTrack && setOpenLyrics(!openLyrics) }} />
style={openLyrics ? "background-color: green;": ""}
icon={new QIcon(musicNode)}
on={{ clicked: () => currentTrack && setOpenLyrics(!openLyrics) }}
/>
<Slider minSize={{ height: 20, width: 80 }} maxSize={{ height: 20, width: 100 }} hasTracking sliderPosition={volume} on={volumeHandler} orientation={Orientation.Horizontal} /> <Slider minSize={{ height: 20, width: 80 }} maxSize={{ height: 20, width: 100 }} hasTracking sliderPosition={volume} on={volumeHandler} orientation={Orientation.Horizontal} />
</BoxView> </BoxView>
</GridColumn> </GridColumn>

View File

@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Image, Text, View } from "@nodegui/react-nodegui"; import { Text, View } from "@nodegui/react-nodegui";
import { QLabel } from "@nodegui/nodegui"; import { QLabel, QPixmap } 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"; import showError from "../../helpers/showError";
@ -10,14 +10,15 @@ interface CachedImageProps extends Omit<ImageProps, "buffer"> {
alt?: string; alt?: string;
} }
function CachedImage({ src, alt, ...props }: CachedImageProps) { function CachedImage({ src, alt, size, maxSize, ...props }: CachedImageProps) {
const imgRef = useRef<QLabel>(); const labelRef = useRef<QLabel>();
const [imageBuffer, setImageBuffer] = useState<Buffer>(); const [imageBuffer, setImageBuffer] = useState<Buffer>();
const [imageProcessError, setImageProcessError] = useState<boolean>(false); const [imageProcessError, setImageProcessError] = useState<boolean>(false);
const pixmap = new QPixmap();
useEffect(() => { useEffect(() => {
if (imageBuffer === undefined) { if (imageBuffer === undefined) {
getCachedImageBuffer(src, props.maxSize ?? props.size) getCachedImageBuffer(src, maxSize ?? size)
.then((buffer) => setImageBuffer(buffer)) .then((buffer) => setImageBuffer(buffer))
.catch((error) => { .catch((error) => {
setImageProcessError(false); setImageProcessError(false);
@ -26,13 +27,22 @@ function CachedImage({ src, alt, ...props }: CachedImageProps) {
} }
return () => { return () => {
imgRef.current?.close(); labelRef.current?.close();
}; };
}, []); }, []);
useEffect(() => {
if (imageBuffer) {
pixmap.loadFromData(imageBuffer);
pixmap.scaled((size ?? maxSize)?.height ?? 100, (size ?? maxSize)?.width ?? 100);
labelRef.current?.setPixmap(pixmap);
}
}, [imageBuffer]);
return !imageProcessError && imageBuffer ? ( return !imageProcessError && imageBuffer ? (
<Image ref={imgRef} buffer={imageBuffer} {...props} /> <Text ref={labelRef} {...props}/>
) : alt ? ( ) : alt ? (
<View style={`padding: ${((props.maxSize ?? props.size)?.height || 10) / 2.5}px ${((props.maxSize ?? props.size)?.width || 10) / 2.5}px;`}> <View style={`padding: ${((maxSize ?? size)?.height || 10) / 2.5}px ${((maxSize ?? size)?.width || 10) / 2.5}px;`}>
<Text>{alt}</Text> <Text>{alt}</Text>
</View> </View>
) : ( ) : (

View File

@ -1,4 +1,5 @@
import React, { Dispatch, SetStateAction } from "react"; import React, { Dispatch, SetStateAction } from "react";
import { Credentials } from "../app";
export interface AuthContext { export interface AuthContext {
isLoggedIn: boolean; isLoggedIn: boolean;
@ -6,6 +7,7 @@ export interface AuthContext {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
access_token: string; access_token: string;
setCredentials: Dispatch<SetStateAction<Credentials>>
setAccess_token: Dispatch<SetStateAction<string>>; setAccess_token: Dispatch<SetStateAction<string>>;
} }
@ -15,6 +17,7 @@ const authContext = React.createContext<AuthContext>({
access_token: "", access_token: "",
clientId: "", clientId: "",
clientSecret: "", clientSecret: "",
setCredentials(){},
setAccess_token() {}, setAccess_token() {},
}); });