mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-14 08:25:16 +00:00
212 lines
7.9 KiB
TypeScript
212 lines
7.9 KiB
TypeScript
import React, { useState, useEffect, useRef } from "react";
|
|
import { Window, hot, View } from "@nodegui/react-nodegui";
|
|
import { QIcon, QMainWindow, WidgetEventTypes, WindowState, QShortcut, QKeySequence } from "@nodegui/nodegui";
|
|
import { MemoryRouter } from "react-router";
|
|
import Routes from "./routes";
|
|
import { LocalStorage } from "node-localstorage";
|
|
import authContext from "./context/authContext";
|
|
import playerContext, { CurrentPlaylist, CurrentTrack } from "./context/playerContext";
|
|
import Player, { audioPlayer } from "./components/Player";
|
|
import { QueryClient, QueryClientProvider } from "react-query";
|
|
import express from "express";
|
|
import open from "open";
|
|
import spotifyApi from "./initializations/spotifyApi";
|
|
import showError from "./helpers/showError";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { confDir, LocalStorageKeys } from "./conf";
|
|
import spotubeIcon from "../assets/icon.svg";
|
|
import preferencesContext, { PreferencesContextProperties } from "./context/preferencesContext";
|
|
|
|
export interface Credentials {
|
|
clientId: string;
|
|
clientSecret: string;
|
|
}
|
|
|
|
const minSize = { width: 700, height: 750 };
|
|
const winIcon = new QIcon(spotubeIcon);
|
|
const localStorageDir = path.join(confDir, "local");
|
|
fs.mkdirSync(localStorageDir, { recursive: true });
|
|
global.localStorage = new LocalStorage(localStorageDir);
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
onError(error) {
|
|
showError(error);
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const initialPreferences: PreferencesContextProperties = {
|
|
playlistImages: false,
|
|
};
|
|
const initialCredentials: Credentials = { clientId: "", clientSecret: "" };
|
|
|
|
//* Application start
|
|
function RootApp() {
|
|
const windowRef = useRef<QMainWindow>();
|
|
const [currentTrack, setCurrentTrack] = useState<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>(() => {
|
|
if (cachedCredentials) {
|
|
return JSON.parse(cachedCredentials);
|
|
}
|
|
return initialCredentials;
|
|
});
|
|
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>();
|
|
|
|
useEffect(() => {
|
|
const parsedCredentials: Credentials = JSON.parse(cachedCredentials ?? "{}");
|
|
setIsLoggedIn(!!(parsedCredentials.clientId && parsedCredentials.clientSecret));
|
|
}, []);
|
|
|
|
// for user code login
|
|
useEffect(() => {
|
|
// 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());
|
|
|
|
app.get<null, null, null, { code: string }>("/auth/spotify/callback", async (req, res) => {
|
|
try {
|
|
spotifyApi.setClientId(credentials.clientId);
|
|
spotifyApi.setClientSecret(credentials.clientSecret);
|
|
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);
|
|
}
|
|
});
|
|
|
|
const server = app.listen(4304, () => {
|
|
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) =>
|
|
console.error("Opening IPC connection with browser failed: ", e)
|
|
);
|
|
});
|
|
return () => {
|
|
server.close(() => console.log("Closed server"));
|
|
};
|
|
}
|
|
}, [credentials]);
|
|
|
|
// just saves the preferences
|
|
useEffect(() => {
|
|
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);
|
|
};
|
|
});
|
|
let spaceShortcut: QShortcut | null;
|
|
let rightShortcut: QShortcut | null;
|
|
let leftShortcut: QShortcut | null;
|
|
// short cut effect
|
|
useEffect(() => {
|
|
if (windowRef.current) {
|
|
spaceShortcut = new QShortcut(windowRef.current);
|
|
rightShortcut = new QShortcut(windowRef.current);
|
|
leftShortcut = new QShortcut(windowRef.current);
|
|
|
|
spaceShortcut.setKey(new QKeySequence("SPACE"));
|
|
rightShortcut.setKey(new QKeySequence("RIGHT"));
|
|
leftShortcut.setKey(new QKeySequence("LEFT"));
|
|
|
|
async function spaceAction() {
|
|
try {
|
|
currentTrack && audioPlayer.isRunning() && (await audioPlayer.isPaused()) ? await audioPlayer.play() : await audioPlayer.pause();
|
|
console.log("You pressed SPACE");
|
|
} catch (error) {
|
|
showError(error, "[Failed to play/pause audioPlayer]: ");
|
|
}
|
|
}
|
|
async function rightAction() {
|
|
try {
|
|
currentTrack && audioPlayer.isRunning() && (await audioPlayer.isSeekable()) && (await audioPlayer.seek(+5));
|
|
console.log("You pressed RIGHT");
|
|
} catch (error) {
|
|
showError(error, "[Failed to seek audioPlayer]: ");
|
|
}
|
|
}
|
|
async function leftAction() {
|
|
try {
|
|
currentTrack && audioPlayer.isRunning() && (await audioPlayer.isSeekable()) && (await audioPlayer.seek(-5));
|
|
console.log("You pressed LEFT");
|
|
} catch (error) {
|
|
showError(error, "[Failed to seek audioPlayer]: ");
|
|
}
|
|
}
|
|
|
|
spaceShortcut.addEventListener("activated", spaceAction);
|
|
rightShortcut.addEventListener("activated", rightAction);
|
|
leftShortcut.addEventListener("activated", leftAction);
|
|
|
|
return () => {
|
|
spaceShortcut?.removeEventListener("activated", spaceAction);
|
|
rightShortcut?.removeEventListener("activated", rightAction);
|
|
leftShortcut?.removeEventListener("activated", leftAction);
|
|
spaceShortcut = null;
|
|
rightShortcut = null;
|
|
leftShortcut = null;
|
|
};
|
|
}
|
|
});
|
|
|
|
return (
|
|
<Window ref={windowRef} windowState={WindowState.WindowMaximized} windowIcon={winIcon} windowTitle="Spotube" minSize={minSize}>
|
|
<MemoryRouter>
|
|
<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}>
|
|
<View style={`flex: 1; flex-direction: 'column'; align-items: 'stretch';`}>
|
|
<Routes />
|
|
{isLoggedIn && <Player />}
|
|
</View>
|
|
</QueryClientProvider>
|
|
</playerContext.Provider>
|
|
</preferencesContext.Provider>
|
|
</authContext.Provider>
|
|
</MemoryRouter>
|
|
</Window>
|
|
);
|
|
}
|
|
|
|
class App extends React.Component {
|
|
render() {
|
|
return <RootApp />;
|
|
}
|
|
}
|
|
|
|
export default hot(App);
|