mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Fixes:
- restart the app to complete login - no volume caching - cached image memory leak
This commit is contained in:
parent
b0c6781df4
commit
4b513f91d8
59
src/app.tsx
59
src/app.tsx
@ -22,6 +22,7 @@ export enum LocalStorageKeys {
|
||||
credentials = "credentials",
|
||||
refresh_token = "refresh_token",
|
||||
preferences = "user-preferences",
|
||||
volume = "volume"
|
||||
}
|
||||
|
||||
export interface Credentials {
|
||||
@ -47,6 +48,7 @@ const queryClient = new QueryClient({
|
||||
const initialPreferences: PreferencesContextProperties = {
|
||||
playlistImages: false,
|
||||
};
|
||||
const initialCredentials: Credentials = { clientId: "", clientSecret: "" };
|
||||
|
||||
//* Application start
|
||||
function RootApp() {
|
||||
@ -87,7 +89,13 @@ function RootApp() {
|
||||
const cachedCredentials = localStorage.getItem(LocalStorageKeys.credentials);
|
||||
// state
|
||||
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>(() => {
|
||||
if (cachedPreferences) {
|
||||
return JSON.parse(cachedPreferences);
|
||||
@ -98,28 +106,15 @@ function RootApp() {
|
||||
const [currentPlaylist, setCurrentPlaylist] = useState<CurrentPlaylist>();
|
||||
|
||||
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
|
||||
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();
|
||||
app.use(express.json());
|
||||
|
||||
@ -130,6 +125,7 @@ function RootApp() {
|
||||
const { body: authRes } = await spotifyApi.authorizationCodeGrant(req.query.code);
|
||||
setAccess_token(authRes.access_token);
|
||||
localStorage.setItem(LocalStorageKeys.refresh_token, authRes.refresh_token);
|
||||
setIsLoggedIn(true);
|
||||
return res.end();
|
||||
} catch (error) {
|
||||
console.error("Failed to fullfil code grant flow: ", error);
|
||||
@ -148,18 +144,31 @@ function RootApp() {
|
||||
server.close(() => console.log("Closed server"));
|
||||
};
|
||||
}
|
||||
}, [isLoggedIn, credentials]);
|
||||
}, [credentials]);
|
||||
|
||||
// just saves the preferences
|
||||
useEffect(() => {
|
||||
if (cachedCredentials) {
|
||||
setCredentials(JSON.parse(cachedCredentials));
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
localStorage.setItem(LocalStorageKeys.preferences, JSON.stringify(preferences));
|
||||
}, [preferences]);
|
||||
|
||||
// window event listeners
|
||||
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);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<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 }}>
|
||||
<authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, setAccess_token, ...credentials, setCredentials }}>
|
||||
<preferencesContext.Provider value={{ ...preferences, setPreferences }}>
|
||||
<playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { LineEdit, Text, Button, View } from "@nodegui/react-nodegui";
|
||||
import authContext from "../context/authContext";
|
||||
import { LocalStorageKeys, Credentials } from "../app";
|
||||
|
||||
function Login() {
|
||||
const { setIsLoggedIn } = useContext(authContext);
|
||||
const { setCredentials: setGlobalCredentials } = useContext(authContext);
|
||||
const [credentials, setCredentials] = useState({
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
@ -54,14 +53,7 @@ function Login() {
|
||||
<Button
|
||||
on={{
|
||||
clicked: () => {
|
||||
localStorage.setItem(
|
||||
LocalStorageKeys.credentials,
|
||||
JSON.stringify({
|
||||
clientId: credentials.clientId,
|
||||
clientSecret: credentials.clientSecret,
|
||||
} as Credentials)
|
||||
);
|
||||
setIsLoggedIn(true);
|
||||
setGlobalCredentials(credentials);
|
||||
},
|
||||
}}
|
||||
text="Add"
|
||||
|
@ -11,6 +11,7 @@ import IconButton from "./shared/IconButton";
|
||||
import showError from "../helpers/showError";
|
||||
import useTrackReaction from "../hooks/useTrackReaction";
|
||||
import ManualLyricDialog from "./ManualLyricDialog";
|
||||
import { LocalStorageKeys } from "../app";
|
||||
|
||||
export const audioPlayer = new NodeMpv(
|
||||
{
|
||||
@ -26,8 +27,9 @@ export const audioPlayer = new NodeMpv(
|
||||
function Player(): ReactElement {
|
||||
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
|
||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||
const cachedVolume = localStorage.getItem(LocalStorageKeys.volume);
|
||||
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 [shuffle, setShuffle] = useState<boolean>(false);
|
||||
const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]);
|
||||
@ -40,10 +42,10 @@ function Player(): ReactElement {
|
||||
setVolume(value);
|
||||
},
|
||||
sliderReleased: () => {
|
||||
localStorage.setItem("volume", volume.toString());
|
||||
localStorage.setItem(LocalStorageKeys.volume, volume.toString());
|
||||
},
|
||||
},
|
||||
[]
|
||||
[volume]
|
||||
);
|
||||
const playerRunning = audioPlayer.isRunning();
|
||||
const titleRef = useRef<QLabel>();
|
||||
@ -223,11 +225,7 @@ function Player(): ReactElement {
|
||||
}}
|
||||
icon={new QIcon(isFavorite(currentTrack?.id ?? "") ? heart : heartRegular)}
|
||||
/>
|
||||
<IconButton
|
||||
style={openLyrics ? "background-color: green;": ""}
|
||||
icon={new QIcon(musicNode)}
|
||||
on={{ clicked: () => currentTrack && setOpenLyrics(!openLyrics) }}
|
||||
/>
|
||||
<IconButton 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} />
|
||||
</BoxView>
|
||||
</GridColumn>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Image, Text, View } from "@nodegui/react-nodegui";
|
||||
import { QLabel } from "@nodegui/nodegui";
|
||||
import { Text, View } from "@nodegui/react-nodegui";
|
||||
import { QLabel, QPixmap } from "@nodegui/nodegui";
|
||||
import { ImageProps } from "@nodegui/react-nodegui/dist/components/Image/RNImage";
|
||||
import { getCachedImageBuffer } from "../../helpers/getCachedImageBuffer";
|
||||
import showError from "../../helpers/showError";
|
||||
@ -10,14 +10,15 @@ interface CachedImageProps extends Omit<ImageProps, "buffer"> {
|
||||
alt?: string;
|
||||
}
|
||||
|
||||
function CachedImage({ src, alt, ...props }: CachedImageProps) {
|
||||
const imgRef = useRef<QLabel>();
|
||||
function CachedImage({ src, alt, size, maxSize, ...props }: CachedImageProps) {
|
||||
const labelRef = useRef<QLabel>();
|
||||
const [imageBuffer, setImageBuffer] = useState<Buffer>();
|
||||
const [imageProcessError, setImageProcessError] = useState<boolean>(false);
|
||||
const pixmap = new QPixmap();
|
||||
|
||||
useEffect(() => {
|
||||
if (imageBuffer===undefined) {
|
||||
getCachedImageBuffer(src, props.maxSize ?? props.size)
|
||||
if (imageBuffer === undefined) {
|
||||
getCachedImageBuffer(src, maxSize ?? size)
|
||||
.then((buffer) => setImageBuffer(buffer))
|
||||
.catch((error) => {
|
||||
setImageProcessError(false);
|
||||
@ -26,13 +27,22 @@ function CachedImage({ src, alt, ...props }: CachedImageProps) {
|
||||
}
|
||||
|
||||
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 ? (
|
||||
<Image ref={imgRef} buffer={imageBuffer} {...props} />
|
||||
<Text ref={labelRef} {...props}/>
|
||||
) : 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>
|
||||
</View>
|
||||
) : (
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { Dispatch, SetStateAction } from "react";
|
||||
import { Credentials } from "../app";
|
||||
|
||||
export interface AuthContext {
|
||||
isLoggedIn: boolean;
|
||||
@ -6,6 +7,7 @@ export interface AuthContext {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
access_token: string;
|
||||
setCredentials: Dispatch<SetStateAction<Credentials>>
|
||||
setAccess_token: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
@ -15,6 +17,7 @@ const authContext = React.createContext<AuthContext>({
|
||||
access_token: "",
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
setCredentials(){},
|
||||
setAccess_token() {},
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user