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
57
src/app.tsx
57
src/app.tsx
@ -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}>
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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() {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user