mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
ESLint, Pretteir configured, Library Followed Artists route
This commit is contained in:
parent
f54ce27d77
commit
7024da3fed
45
.eslintrc.js
Normal file
45
.eslintrc.js
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @type {import("eslint").Linter.Config}
|
||||
*/
|
||||
const config = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: [
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
plugins: ["react", "@typescript-eslint"],
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "16.14.0",
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
Atomics: "readonly",
|
||||
SharedArrayBuffer: "readonly",
|
||||
},
|
||||
ignorePatterns: [".eslintrc.js", "./src/reportWebVitals.ts"],
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 2018,
|
||||
sourceType: "module",
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"prettier/prettier": "warn",
|
||||
"react/prop-types": "off",
|
||||
"linebreak-style": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
11
.prettierrc.js
Normal file
11
.prettierrc.js
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* @type {import("prettier").Options}
|
||||
*/
|
||||
const config = {
|
||||
tabWidth: 4,
|
||||
printWidth: 90,
|
||||
semi: true,
|
||||
endOfLine: "auto",
|
||||
trailingComma: "all",
|
||||
};
|
||||
module.exports = config;
|
4098
package-lock.json
generated
4098
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -7,8 +7,8 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "webpack --mode=production",
|
||||
"dev": "nodemon -w src/** -e ts,tsx -x 'node esbuild.config.mjs'",
|
||||
"check-types": "tsc --noEmit --watch",
|
||||
"dev": "nodemon -w src/ -e ts,tsx -x 'node esbuild.config.mjs'",
|
||||
"check-types": "nodemon --quiet -e tsx,ts -w src/ -x tsc --noEmit --pretty",
|
||||
"start": "cd dist && qode index.js",
|
||||
"start:watch": "nodemon -w dist -e js -x \"npm start\"",
|
||||
"start-dev": "concurrently -n \"esbuild,spotube,tsc\" -p \"{name}\" -c \"bgYellow.black.bold,bgGreen.black.bold,bgBlue.black.bold\" -i --default-input-target spotube \"npm run dev\" \"npm run start:watch\" \"npm run check-types\"",
|
||||
@ -60,16 +60,26 @@
|
||||
"@types/spotify-web-api-node": "^5.0.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/webpack-env": "^1.15.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.27.0",
|
||||
"@typescript-eslint/parser": "^4.27.0",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.3",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"concurrently": "^6.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.12.8",
|
||||
"esbuild-loader": "^2.13.1",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.2.0",
|
||||
"native-addon-loader": "^2.0.1",
|
||||
"nodemon": "^2.0.7",
|
||||
"prettier": "^2.3.1",
|
||||
"typescript": "^4.2.3",
|
||||
"webpack": "^5.27.0",
|
||||
"webpack-cli": "^4.4.0"
|
||||
|
396
src/app.tsx
396
src/app.tsx
@ -1,6 +1,13 @@
|
||||
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 {
|
||||
QIcon,
|
||||
QMainWindow,
|
||||
WidgetEventTypes,
|
||||
WindowState,
|
||||
QShortcut,
|
||||
QKeySequence,
|
||||
} from "@nodegui/nodegui";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import Routes from "./routes";
|
||||
import { LocalStorage } from "node-localstorage";
|
||||
@ -16,11 +23,13 @@ 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";
|
||||
import preferencesContext, {
|
||||
PreferencesContextProperties,
|
||||
} from "./context/preferencesContext";
|
||||
|
||||
export interface Credentials {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
|
||||
const minSize = { width: 700, height: 750 };
|
||||
@ -29,183 +38,244 @@ 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);
|
||||
},
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
onError(error) {
|
||||
showError(error);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const initialPreferences: PreferencesContextProperties = {
|
||||
playlistImages: false,
|
||||
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 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 [credentials, setCredentials] = useState<Credentials>(() => {
|
||||
if (cachedCredentials) {
|
||||
return JSON.parse(cachedCredentials);
|
||||
}
|
||||
});
|
||||
|
||||
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]: ");
|
||||
return initialCredentials;
|
||||
});
|
||||
const [preferences, setPreferences] = useState<PreferencesContextProperties>(() => {
|
||||
if (cachedPreferences) {
|
||||
return JSON.parse(cachedPreferences);
|
||||
}
|
||||
}
|
||||
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]: ");
|
||||
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"));
|
||||
};
|
||||
}
|
||||
}
|
||||
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]: ");
|
||||
}, [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;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
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 />;
|
||||
}
|
||||
render() {
|
||||
return <RootApp />;
|
||||
}
|
||||
}
|
||||
|
||||
export default hot(App);
|
||||
|
10
src/components/Artist.tsx
Normal file
10
src/components/Artist.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { ScrollArea } from "@nodegui/react-nodegui";
|
||||
import React from "react";
|
||||
|
||||
function Artist() {
|
||||
return (
|
||||
<ScrollArea style="min-height: 750px; max-height: 1980px; max-width: 1980px; min-width: 700px; border: none;"></ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
export default Artist;
|
@ -5,9 +5,15 @@ import { angleLeft } from "../icons";
|
||||
import IconButton from "./shared/IconButton";
|
||||
|
||||
function BackButton(): ReactElement {
|
||||
const history = useHistory();
|
||||
const history = useHistory();
|
||||
|
||||
return <IconButton style={"align-self: flex-start;"} icon={new QIcon(angleLeft)} on={{ clicked: () => history.goBack() }} />;
|
||||
return (
|
||||
<IconButton
|
||||
style={"align-self: flex-start;"}
|
||||
icon={new QIcon(angleLeft)}
|
||||
on={{ clicked: () => history.goBack() }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default BackButton;
|
||||
|
@ -5,31 +5,39 @@ import { TrackTableIndex } from "./PlaylistView";
|
||||
import { TrackButton } from "./shared/TrackButton";
|
||||
|
||||
function CurrentPlaylist() {
|
||||
const { currentPlaylist, currentTrack } = useContext(playerContext);
|
||||
const { currentPlaylist, currentTrack } = useContext(playerContext);
|
||||
|
||||
if (!currentPlaylist && !currentTrack) {
|
||||
return <Text style="flex: 1;">{`<center>There is nothing being played now</center>`}</Text>;
|
||||
}
|
||||
if (!currentPlaylist && !currentTrack) {
|
||||
return (
|
||||
<Text style="flex: 1;">{`<center>There is nothing being played now</center>`}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentTrack && !currentPlaylist) {
|
||||
<View style="flex: 1;">
|
||||
<TrackButton track={currentTrack} index={0}/>
|
||||
</View>
|
||||
}
|
||||
if (currentTrack && !currentPlaylist) {
|
||||
<View style="flex: 1;">
|
||||
<TrackButton track={currentTrack} index={0} />
|
||||
</View>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text>{`<center><h2>${currentPlaylist?.name}</h2></center>`}</Text>
|
||||
<TrackTableIndex />
|
||||
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
||||
<View style={`flex-direction:column; flex: 1;`}>
|
||||
{currentPlaylist?.tracks.map(({ track }, index) => {
|
||||
return <TrackButton key={index + track.id} track={track} index={index} />;
|
||||
})}
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text>{`<center><h2>${currentPlaylist?.name}</h2></center>`}</Text>
|
||||
<TrackTableIndex />
|
||||
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
||||
<View style={`flex-direction:column; flex: 1;`}>
|
||||
{currentPlaylist?.tracks.map(({ track }, index) => {
|
||||
return (
|
||||
<TrackButton
|
||||
key={index + track.id}
|
||||
track={track}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export default CurrentPlaylist;
|
||||
|
@ -7,58 +7,96 @@ import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
||||
import CategoryCardView from "./shared/CategoryCardView";
|
||||
|
||||
function Home() {
|
||||
const { data: pagedCategories, isError, refetch, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } = useSpotifyInfiniteQuery<SpotifyApi.PagingObject<SpotifyApi.CategoryObject>>(
|
||||
QueryCacheKeys.categories,
|
||||
(spotifyApi, { pageParam }) => spotifyApi.getCategories({ country: "US", limit: 10, offset: pageParam }).then((categoriesReceived) => categoriesReceived.body.categories),
|
||||
{
|
||||
getNextPageParam(lastPage) {
|
||||
if (lastPage.next) {
|
||||
return lastPage.offset + lastPage.limit;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
const {
|
||||
data: pagedCategories,
|
||||
isError,
|
||||
refetch,
|
||||
isLoading,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
} = useSpotifyInfiniteQuery<SpotifyApi.PagingObject<SpotifyApi.CategoryObject>>(
|
||||
QueryCacheKeys.categories,
|
||||
(spotifyApi, { pageParam }) =>
|
||||
spotifyApi
|
||||
.getCategories({ country: "US", limit: 10, offset: pageParam })
|
||||
.then((categoriesReceived) => categoriesReceived.body.categories),
|
||||
{
|
||||
getNextPageParam(lastPage) {
|
||||
if (lastPage.next) {
|
||||
return lastPage.offset + lastPage.limit;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const categories = pagedCategories?.pages
|
||||
.map((page) => page.items)
|
||||
.filter(Boolean)
|
||||
.flat(1);
|
||||
categories?.unshift({ href: "", icons: [], id: "featured", name: "Featured" });
|
||||
return (
|
||||
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
||||
<View style={`flex-direction: 'column'; align-items: 'center'; flex: 1;`}>
|
||||
<PlaceholderApplet error={isError} message="Failed to query genres" reload={refetch} helps loading={isLoading} />
|
||||
{categories?.map((category, index) => {
|
||||
return <CategoryCard key={index + category.id} id={category.id} name={category.name} />;
|
||||
})}
|
||||
{hasNextPage && <Button on={{ clicked: () => fetchNextPage() }} text="Load More" enabled={!isFetchingNextPage} />}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
);
|
||||
const categories = pagedCategories?.pages
|
||||
.map((page) => page.items)
|
||||
.filter(Boolean)
|
||||
.flat(1);
|
||||
categories?.unshift({ href: "", icons: [], id: "featured", name: "Featured" });
|
||||
return (
|
||||
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
||||
<View style={`flex-direction: 'column'; align-items: 'center'; flex: 1;`}>
|
||||
<PlaceholderApplet
|
||||
error={isError}
|
||||
message="Failed to query genres"
|
||||
reload={refetch}
|
||||
helps
|
||||
loading={isLoading}
|
||||
/>
|
||||
{categories?.map((category, index) => {
|
||||
return (
|
||||
<CategoryCard
|
||||
key={index + category.id}
|
||||
id={category.id}
|
||||
name={category.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
on={{ clicked: () => fetchNextPage() }}
|
||||
text="Load More"
|
||||
enabled={!isFetchingNextPage}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
|
||||
interface CategoryCardProps {
|
||||
id: string;
|
||||
name: string;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const CategoryCard = ({ id, name }: CategoryCardProps) => {
|
||||
const { data: playlists, isError } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(
|
||||
[QueryCacheKeys.categoryPlaylists, id],
|
||||
async (spotifyApi) => {
|
||||
const option = { limit: 4 };
|
||||
let res;
|
||||
if (id === "featured") {
|
||||
res = await spotifyApi.getFeaturedPlaylists(option);
|
||||
} else {
|
||||
res = await spotifyApi.getPlaylistsForCategory(id, option);
|
||||
}
|
||||
return res.body.playlists.items;
|
||||
},
|
||||
{ initialData: [] }
|
||||
);
|
||||
const { data: playlists, isError } = useSpotifyQuery<
|
||||
SpotifyApi.PlaylistObjectSimplified[]
|
||||
>(
|
||||
[QueryCacheKeys.categoryPlaylists, id],
|
||||
async (spotifyApi) => {
|
||||
const option = { limit: 4 };
|
||||
let res;
|
||||
if (id === "featured") {
|
||||
res = await spotifyApi.getFeaturedPlaylists(option);
|
||||
} else {
|
||||
res = await spotifyApi.getPlaylistsForCategory(id, option);
|
||||
}
|
||||
return res.body.playlists.items;
|
||||
},
|
||||
{ initialData: [] },
|
||||
);
|
||||
|
||||
return <CategoryCardView url={`/genre/playlists/${id}`} isError={isError} name={name} playlists={playlists ?? []} />;
|
||||
return (
|
||||
<CategoryCardView
|
||||
url={`/genre/playlists/${id}`}
|
||||
isError={isError}
|
||||
name={name}
|
||||
playlists={playlists ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Button, ScrollArea, View } from "@nodegui/react-nodegui";
|
||||
import { CursorShape } from "@nodegui/nodegui";
|
||||
import { Button, ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||
import React, { useContext } from "react";
|
||||
import { Redirect, Route } from "react-router";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
@ -6,139 +7,279 @@ import playerContext from "../context/playerContext";
|
||||
import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
||||
import { GenreView } from "./PlaylistGenreView";
|
||||
import { PlaylistSimpleControls, TrackTableIndex } from "./PlaylistView";
|
||||
import CachedImage from "./shared/CachedImage";
|
||||
import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||
import { TrackButton, TrackButtonPlaylistObject } from "./shared/TrackButton";
|
||||
import { TabMenuItem } from "./TabMenu";
|
||||
|
||||
function Library() {
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Redirect from="/library" to="/library/saved-tracks" />
|
||||
<View style="max-width: 350px; justify-content: 'space-evenly'">
|
||||
<TabMenuItem title="Saved Tracks" url="/library/saved-tracks" />
|
||||
<TabMenuItem title="Playlists" url="/library/playlists" />
|
||||
</View>
|
||||
<Route exact path="/library/saved-tracks">
|
||||
<UserSavedTracks />
|
||||
</Route>
|
||||
<Route exact path="/library/playlists">
|
||||
<UserPlaylists />
|
||||
</Route>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Redirect from="/library" to="/library/saved-tracks" />
|
||||
<View style="max-width: 350px; justify-content: 'space-evenly'">
|
||||
<TabMenuItem title="Saved Tracks" url="/library/saved-tracks" />
|
||||
<TabMenuItem title="Playlists" url="/library/playlists" />
|
||||
<TabMenuItem title="Artists" url="/library/followed-artists" />
|
||||
</View>
|
||||
<Route exact path="/library/saved-tracks">
|
||||
<UserSavedTracks />
|
||||
</Route>
|
||||
<Route exact path="/library/playlists">
|
||||
<UserPlaylists />
|
||||
</Route>
|
||||
<Route exact path="/library/followed-artists">
|
||||
<FollowedArtists />
|
||||
</Route>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default Library;
|
||||
|
||||
function UserPlaylists() {
|
||||
const { data: userPagedPlaylists, isError, isLoading, refetch, isFetchingNextPage, hasNextPage, fetchNextPage } = useSpotifyInfiniteQuery<SpotifyApi.ListOfUsersPlaylistsResponse>(
|
||||
QueryCacheKeys.userPlaylists,
|
||||
(spotifyApi, { pageParam }) =>
|
||||
spotifyApi.getUserPlaylists({ limit: 20, offset: pageParam }).then((userPlaylists) => {
|
||||
return userPlaylists.body;
|
||||
}),
|
||||
{
|
||||
getNextPageParam(lastPage) {
|
||||
if (lastPage.next) {
|
||||
return lastPage.offset + lastPage.limit;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
const {
|
||||
data: userPagedPlaylists,
|
||||
isError,
|
||||
isLoading,
|
||||
refetch,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
} = useSpotifyInfiniteQuery<SpotifyApi.ListOfUsersPlaylistsResponse>(
|
||||
QueryCacheKeys.userPlaylists,
|
||||
(spotifyApi, { pageParam }) =>
|
||||
spotifyApi
|
||||
.getUserPlaylists({ limit: 20, offset: pageParam })
|
||||
.then((userPlaylists) => {
|
||||
return userPlaylists.body;
|
||||
}),
|
||||
{
|
||||
getNextPageParam(lastPage) {
|
||||
if (lastPage.next) {
|
||||
return lastPage.offset + lastPage.limit;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const userPlaylists = userPagedPlaylists?.pages
|
||||
?.map((playlist) => playlist.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.PlaylistObjectSimplified[];
|
||||
const userPlaylists = userPagedPlaylists?.pages
|
||||
?.map((playlist) => playlist.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.PlaylistObjectSimplified[];
|
||||
|
||||
return (
|
||||
<GenreView
|
||||
heading="User Playlists"
|
||||
isError={isError}
|
||||
isLoading={isLoading}
|
||||
playlists={userPlaylists ?? []}
|
||||
isLoadable={!isFetchingNextPage}
|
||||
refetch={refetch}
|
||||
loadMore={hasNextPage ? ()=>fetchNextPage() : undefined}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<GenreView
|
||||
heading="User Playlists"
|
||||
isError={isError}
|
||||
isLoading={isLoading}
|
||||
playlists={userPlaylists ?? []}
|
||||
isLoadable={!isFetchingNextPage}
|
||||
refetch={refetch}
|
||||
loadMore={hasNextPage ? () => fetchNextPage() : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function UserSavedTracks() {
|
||||
const userSavedPlaylistId = "user-saved-tracks";
|
||||
const { data: userSavedTracks, fetchNextPage, hasNextPage, isFetchingNextPage, isError, isLoading, refetch } = useSpotifyInfiniteQuery<SpotifyApi.UsersSavedTracksResponse>(
|
||||
QueryCacheKeys.userSavedTracks,
|
||||
(spotifyApi, { pageParam }) => spotifyApi.getMySavedTracks({ limit: 50, offset: pageParam }).then((res) => res.body),
|
||||
{
|
||||
getNextPageParam(lastPage) {
|
||||
if (lastPage.next) {
|
||||
return lastPage.offset + lastPage.limit;
|
||||
const userSavedPlaylistId = "user-saved-tracks";
|
||||
const {
|
||||
data: userSavedTracks,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isError,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useSpotifyInfiniteQuery<SpotifyApi.UsersSavedTracksResponse>(
|
||||
QueryCacheKeys.userSavedTracks,
|
||||
(spotifyApi, { pageParam }) =>
|
||||
spotifyApi
|
||||
.getMySavedTracks({ limit: 50, offset: pageParam })
|
||||
.then((res) => res.body),
|
||||
{
|
||||
getNextPageParam(lastPage) {
|
||||
if (lastPage.next) {
|
||||
return lastPage.offset + lastPage.limit;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack } =
|
||||
useContext(playerContext);
|
||||
|
||||
const userTracks = userSavedTracks?.pages
|
||||
?.map((page) => page.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.SavedTrackObject[] | undefined;
|
||||
|
||||
function handlePlaylistPlayPause(index?: number) {
|
||||
if (currentPlaylist?.id !== userSavedPlaylistId && userTracks) {
|
||||
setCurrentPlaylist({
|
||||
id: userSavedPlaylistId,
|
||||
name: "Liked Tracks",
|
||||
thumbnail:
|
||||
"https://nerdist.com/wp-content/uploads/2020/07/maxresdefault.jpg",
|
||||
tracks: userTracks,
|
||||
});
|
||||
setCurrentTrack(userTracks[index ?? 0].track);
|
||||
} else {
|
||||
setCurrentPlaylist(undefined);
|
||||
setCurrentTrack(undefined);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack } = useContext(playerContext);
|
||||
|
||||
const userTracks = userSavedTracks?.pages
|
||||
?.map((page) => page.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.SavedTrackObject[] | undefined;
|
||||
|
||||
function handlePlaylistPlayPause(index?: number) {
|
||||
if (currentPlaylist?.id !== userSavedPlaylistId && userTracks) {
|
||||
setCurrentPlaylist({ id: userSavedPlaylistId, name: "Liked Tracks", thumbnail: "https://nerdist.com/wp-content/uploads/2020/07/maxresdefault.jpg", tracks: userTracks });
|
||||
setCurrentTrack(userTracks[index ?? 0].track);
|
||||
} else {
|
||||
setCurrentPlaylist(undefined);
|
||||
setCurrentTrack(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const playlist: TrackButtonPlaylistObject = {
|
||||
collaborative: false,
|
||||
description: "User Playlist",
|
||||
tracks: {
|
||||
items: userTracks ?? [],
|
||||
limit: 20,
|
||||
href: "",
|
||||
next: "",
|
||||
offset: 0,
|
||||
previous: "",
|
||||
total: 20,
|
||||
},
|
||||
external_urls: { spotify: "" },
|
||||
href: "",
|
||||
id: userSavedPlaylistId,
|
||||
images: [{ url: "https://facebook.com/img.jpeg" }],
|
||||
name: "User saved track",
|
||||
owner: { external_urls: { spotify: "" }, href: "", id: "Me", type: "user", uri: "spotify:user:me", display_name: "User", followers: { href: null, total: 0 } },
|
||||
public: false,
|
||||
snapshot_id: userSavedPlaylistId + "snapshot",
|
||||
type: "playlist",
|
||||
uri: "spotify:user:me:saved-tracks",
|
||||
};
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<PlaylistSimpleControls handlePlaylistPlayPause={handlePlaylistPlayPause} isActive={currentPlaylist?.id === userSavedPlaylistId} />
|
||||
<TrackTableIndex />
|
||||
<ScrollArea style="flex: 1; border: none;">
|
||||
<View style="flex: 1; flex-direction: 'column'; align-items: 'stretch';">
|
||||
<PlaceholderApplet error={isError} loading={isLoading || isFetchingNextPage} message="Failed querying spotify" reload={refetch} />
|
||||
{userTracks?.map(({ track }, index) => track && <TrackButton key={index + track.id} track={track} index={index} playlist={playlist} />)}
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
style="flex-grow: 0; align-self: 'center';"
|
||||
text="Load more"
|
||||
on={{
|
||||
clicked() {
|
||||
fetchNextPage();
|
||||
},
|
||||
}}
|
||||
enabled={!isFetchingNextPage}
|
||||
const playlist: TrackButtonPlaylistObject = {
|
||||
collaborative: false,
|
||||
description: "User Playlist",
|
||||
tracks: {
|
||||
items: userTracks ?? [],
|
||||
limit: 20,
|
||||
href: "",
|
||||
next: "",
|
||||
offset: 0,
|
||||
previous: "",
|
||||
total: 20,
|
||||
},
|
||||
external_urls: { spotify: "" },
|
||||
href: "",
|
||||
id: userSavedPlaylistId,
|
||||
images: [{ url: "https://facebook.com/img.jpeg" }],
|
||||
name: "User saved track",
|
||||
owner: {
|
||||
external_urls: { spotify: "" },
|
||||
href: "",
|
||||
id: "Me",
|
||||
type: "user",
|
||||
uri: "spotify:user:me",
|
||||
display_name: "User",
|
||||
followers: { href: null, total: 0 },
|
||||
},
|
||||
public: false,
|
||||
snapshot_id: userSavedPlaylistId + "snapshot",
|
||||
type: "playlist",
|
||||
uri: "spotify:user:me:saved-tracks",
|
||||
};
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<PlaylistSimpleControls
|
||||
handlePlaylistPlayPause={handlePlaylistPlayPause}
|
||||
isActive={currentPlaylist?.id === userSavedPlaylistId}
|
||||
/>
|
||||
)}
|
||||
<TrackTableIndex />
|
||||
<ScrollArea style="flex: 1; border: none;">
|
||||
<View style="flex: 1; flex-direction: 'column'; align-items: 'stretch';">
|
||||
<PlaceholderApplet
|
||||
error={isError}
|
||||
loading={isLoading || isFetchingNextPage}
|
||||
message="Failed querying spotify"
|
||||
reload={refetch}
|
||||
/>
|
||||
{userTracks?.map(
|
||||
({ track }, index) =>
|
||||
track && (
|
||||
<TrackButton
|
||||
key={index + track.id}
|
||||
track={track}
|
||||
index={index}
|
||||
playlist={playlist}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
style="flex-grow: 0; align-self: 'center';"
|
||||
text="Load more"
|
||||
on={{
|
||||
clicked() {
|
||||
fetchNextPage();
|
||||
},
|
||||
}}
|
||||
enabled={!isFetchingNextPage}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
function FollowedArtists() {
|
||||
const {
|
||||
data: pagedFollowedArtists,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isError,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useSpotifyInfiniteQuery<
|
||||
SpotifyApi.CursorBasedPagingObject<SpotifyApi.ArtistObjectFull>
|
||||
>(
|
||||
QueryCacheKeys.followedArtists,
|
||||
(spotifyApi, { pageParam }) =>
|
||||
spotifyApi
|
||||
.getFollowedArtists({ limit: 50, after: pageParam })
|
||||
.then((res) => res.body.artists),
|
||||
{
|
||||
getNextPageParam(lastPage) {
|
||||
if (lastPage.next) {
|
||||
return lastPage.cursors.after + lastPage.limit;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const followedArtists = pagedFollowedArtists?.pages
|
||||
?.map((page) => page.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.ArtistObjectFull[] | undefined;
|
||||
|
||||
return (
|
||||
<ScrollArea style="min-height: 750px; max-height: 1980px; max-width: 1980px; min-width: 700px; border: none;">
|
||||
<View style="flex: 1; flex-direction: 'row'; flex-wrap: wrap; width: 330px;">
|
||||
<PlaceholderApplet
|
||||
error={isError}
|
||||
loading={isLoading || isFetchingNextPage}
|
||||
message="Failed querying spotify"
|
||||
reload={refetch}
|
||||
/>
|
||||
|
||||
{followedArtists?.map((artist, index) => {
|
||||
return <ArtistCard key={index + artist.id} artist={artist} />;
|
||||
})}
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
style="flex-grow: 0; align-self: 'center';"
|
||||
text="Load more"
|
||||
on={{
|
||||
clicked() {
|
||||
fetchNextPage();
|
||||
},
|
||||
}}
|
||||
enabled={!isFetchingNextPage}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
interface ArtistCardProps {
|
||||
artist: SpotifyApi.ArtistObjectFull;
|
||||
}
|
||||
|
||||
function ArtistCard({ artist }: ArtistCardProps) {
|
||||
return (
|
||||
<View style="max-width: 150px; max-height: 200px; flex-direction: 'column'; align-items: 'center'; margin: 5px 0;">
|
||||
<CachedImage
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
maxSize={{ height: 150, width: 150 }}
|
||||
scaledContents
|
||||
alt={artist.name}
|
||||
src={artist.images[0].url}
|
||||
/>
|
||||
<Text>{artist.name}</Text>
|
||||
<Button cursor={CursorShape.PointingHandCursor} text="Follow" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -3,63 +3,63 @@ import { LineEdit, Text, Button, View } from "@nodegui/react-nodegui";
|
||||
import authContext from "../context/authContext";
|
||||
|
||||
function Login() {
|
||||
const { setCredentials: setGlobalCredentials } = useContext(authContext);
|
||||
const [credentials, setCredentials] = useState({
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
});
|
||||
const { setCredentials: setGlobalCredentials } = useContext(authContext);
|
||||
const [credentials, setCredentials] = useState({
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
});
|
||||
|
||||
const [touched, setTouched] = useState({
|
||||
clientId: false,
|
||||
clientSecret: false,
|
||||
});
|
||||
const [touched, setTouched] = useState({
|
||||
clientId: false,
|
||||
clientSecret: false,
|
||||
});
|
||||
|
||||
type fieldNames = "clientId" | "clientSecret";
|
||||
type fieldNames = "clientId" | "clientSecret";
|
||||
|
||||
function textChanged(text: string, fieldName: fieldNames) {
|
||||
setCredentials({ ...credentials, [fieldName]: text });
|
||||
}
|
||||
|
||||
function textEdited(name: fieldNames) {
|
||||
if (!touched[name]) {
|
||||
setTouched({ ...touched, [name]: true });
|
||||
function textChanged(text: string, fieldName: fieldNames) {
|
||||
setCredentials({ ...credentials, [fieldName]: text });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={`flex: 1; flex-direction: 'column';`}>
|
||||
<Text>{`<center><h1>Add Spotify & Youtube credentials to get started</h1></center>`}</Text>
|
||||
<Text>{`<center><p>Don't worry any of the credentials won't be collected or used for abuses</p></center>`}</Text>
|
||||
<LineEdit
|
||||
on={{
|
||||
textChanged: (t) => textChanged(t, "clientId"),
|
||||
textEdited() {
|
||||
textEdited("clientId");
|
||||
},
|
||||
}}
|
||||
text={credentials.clientId}
|
||||
placeholderText="spotify clientId"
|
||||
/>
|
||||
<LineEdit
|
||||
on={{
|
||||
textChanged: (t) => textChanged(t, "clientSecret"),
|
||||
textEdited() {
|
||||
textEdited("clientSecret");
|
||||
},
|
||||
}}
|
||||
text={credentials.clientSecret}
|
||||
placeholderText="spotify clientSecret"
|
||||
/>
|
||||
<Button
|
||||
on={{
|
||||
clicked: () => {
|
||||
setGlobalCredentials(credentials);
|
||||
},
|
||||
}}
|
||||
text="Add"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
function textEdited(name: fieldNames) {
|
||||
if (!touched[name]) {
|
||||
setTouched({ ...touched, [name]: true });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={`flex: 1; flex-direction: 'column';`}>
|
||||
<Text>{`<center><h1>Add Spotify & Youtube credentials to get started</h1></center>`}</Text>
|
||||
<Text>{`<center><p>Don't worry any of the credentials won't be collected or used for abuses</p></center>`}</Text>
|
||||
<LineEdit
|
||||
on={{
|
||||
textChanged: (t) => textChanged(t, "clientId"),
|
||||
textEdited() {
|
||||
textEdited("clientId");
|
||||
},
|
||||
}}
|
||||
text={credentials.clientId}
|
||||
placeholderText="spotify clientId"
|
||||
/>
|
||||
<LineEdit
|
||||
on={{
|
||||
textChanged: (t) => textChanged(t, "clientSecret"),
|
||||
textEdited() {
|
||||
textEdited("clientSecret");
|
||||
},
|
||||
}}
|
||||
text={credentials.clientSecret}
|
||||
placeholderText="spotify clientSecret"
|
||||
/>
|
||||
<Button
|
||||
on={{
|
||||
clicked: () => {
|
||||
setGlobalCredentials(credentials);
|
||||
},
|
||||
}}
|
||||
text="Add"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
|
@ -1,89 +1,97 @@
|
||||
import { FlexLayout, QDialog, QLabel, QPushButton, QScrollArea, QWidget, TextFormat } from "@nodegui/nodegui";
|
||||
import {
|
||||
FlexLayout,
|
||||
QDialog,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QWidget,
|
||||
TextFormat,
|
||||
} from "@nodegui/nodegui";
|
||||
import React, { PropsWithChildren, useEffect, useState } from "react";
|
||||
import showError from "../helpers/showError";
|
||||
import fetchLyrics from "../helpers/fetchLyrics";
|
||||
|
||||
interface ManualLyricDialogProps extends PropsWithChildren<{}> {
|
||||
open: boolean;
|
||||
onClose?: (closed: boolean) => void;
|
||||
track: SpotifyApi.TrackObjectSimplified | SpotifyApi.TrackObjectFull;
|
||||
interface ManualLyricDialogProps extends PropsWithChildren<unknown> {
|
||||
open: boolean;
|
||||
onClose?: (closed: boolean) => void;
|
||||
track: SpotifyApi.TrackObjectSimplified | SpotifyApi.TrackObjectFull;
|
||||
}
|
||||
|
||||
function ManualLyricDialog({ open, track, onClose }: ManualLyricDialogProps) {
|
||||
const dialog = new QDialog();
|
||||
const areaContainer = new QWidget();
|
||||
const retryButton = new QPushButton();
|
||||
const scrollArea = new QScrollArea();
|
||||
const titleLabel = new QLabel();
|
||||
const lyricLabel = new QLabel();
|
||||
const [lyricNotFound, setLyricNotFound] = useState<boolean>(false);
|
||||
const [lyrics, setLyrics] = useState<string>("");
|
||||
const artists = track.artists.map((artist) => artist.name).join(", ");
|
||||
function ManualLyricDialog({ open, track }: ManualLyricDialogProps) {
|
||||
const dialog = new QDialog();
|
||||
const areaContainer = new QWidget();
|
||||
const retryButton = new QPushButton();
|
||||
const scrollArea = new QScrollArea();
|
||||
const titleLabel = new QLabel();
|
||||
const lyricLabel = new QLabel();
|
||||
const [lyricNotFound, setLyricNotFound] = useState<boolean>(false);
|
||||
const [lyrics, setLyrics] = useState<string>("");
|
||||
const artists = track.artists.map((artist) => artist.name).join(", ");
|
||||
|
||||
async function handleBtnClick() {
|
||||
try {
|
||||
const lyrics = await fetchLyrics(artists, track.name);
|
||||
console.log('lyrics:', lyrics)
|
||||
setLyrics(lyrics);
|
||||
setLyricNotFound(lyrics === "Not Found");
|
||||
} catch (error) {
|
||||
showError(error, `[Finding lyrics for ${track.name} failed]: `);
|
||||
setLyrics("No lyrics found, rare track :)");
|
||||
setLyricNotFound(true);
|
||||
async function handleBtnClick() {
|
||||
try {
|
||||
const lyrics = await fetchLyrics(artists, track.name);
|
||||
console.log("lyrics:", lyrics);
|
||||
setLyrics(lyrics);
|
||||
setLyricNotFound(lyrics === "Not Found");
|
||||
} catch (error) {
|
||||
showError(error, `[Finding lyrics for ${track.name} failed]: `);
|
||||
setLyrics("No lyrics found, rare track :)");
|
||||
setLyricNotFound(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// title label
|
||||
titleLabel.setText(`
|
||||
useEffect(() => {
|
||||
// title label
|
||||
titleLabel.setText(`
|
||||
<center>
|
||||
<h2>${track.name}</h2>
|
||||
<p>- ${artists}</p>
|
||||
</center>
|
||||
`);
|
||||
// lyric label
|
||||
lyricLabel.setText(lyrics);
|
||||
lyricLabel.setTextFormat(TextFormat.PlainText);
|
||||
// area container
|
||||
areaContainer.setLayout(new FlexLayout());
|
||||
areaContainer.setInlineStyle("flex: 1; flex-direction: 'column'; padding: 10px;");
|
||||
areaContainer.layout?.addWidget(titleLabel);
|
||||
areaContainer.layout?.addWidget(lyricLabel);
|
||||
areaContainer.layout?.addWidget(retryButton);
|
||||
// scroll area
|
||||
scrollArea.setInlineStyle("flex: 1;");
|
||||
scrollArea.setWidget(areaContainer);
|
||||
// lyric label
|
||||
lyricLabel.setText(lyrics);
|
||||
lyricLabel.setTextFormat(TextFormat.PlainText);
|
||||
// area container
|
||||
areaContainer.setLayout(new FlexLayout());
|
||||
areaContainer.setInlineStyle("flex: 1; flex-direction: 'column'; padding: 10px;");
|
||||
areaContainer.layout?.addWidget(titleLabel);
|
||||
areaContainer.layout?.addWidget(lyricLabel);
|
||||
areaContainer.layout?.addWidget(retryButton);
|
||||
// scroll area
|
||||
scrollArea.setInlineStyle("flex: 1;");
|
||||
scrollArea.setWidget(areaContainer);
|
||||
|
||||
// reload button
|
||||
retryButton.setText("Retry");
|
||||
retryButton.addEventListener("clicked", handleBtnClick);
|
||||
// dialog
|
||||
dialog.setWindowTitle("Lyrics");
|
||||
dialog.setLayout(new FlexLayout());
|
||||
dialog.layout?.addWidget(scrollArea);
|
||||
open ? dialog.open() : dialog.close();
|
||||
open &&
|
||||
fetchLyrics(artists, track.name)
|
||||
.then((lyrics: string) => {
|
||||
setLyrics(lyrics);
|
||||
setLyricNotFound(lyrics === "Not Found");
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
showError(e, `[Finding lyrics for ${track.name} failed]: `);
|
||||
setLyrics("No lyrics found, rare track :)");
|
||||
setLyricNotFound(true);
|
||||
});
|
||||
return () => {
|
||||
retryButton.removeEventListener("clicked", handleBtnClick);
|
||||
dialog.hide();
|
||||
};
|
||||
}, [open, track, lyrics]);
|
||||
// reload button
|
||||
retryButton.setText("Retry");
|
||||
retryButton.addEventListener("clicked", handleBtnClick);
|
||||
// dialog
|
||||
dialog.setWindowTitle("Lyrics");
|
||||
dialog.setLayout(new FlexLayout());
|
||||
dialog.layout?.addWidget(scrollArea);
|
||||
open ? dialog.open() : dialog.close();
|
||||
open &&
|
||||
fetchLyrics(artists, track.name)
|
||||
.then((lyrics: string) => {
|
||||
setLyrics(lyrics);
|
||||
setLyricNotFound(lyrics === "Not Found");
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
showError(e, `[Finding lyrics for ${track.name} failed]: `);
|
||||
setLyrics("No lyrics found, rare track :)");
|
||||
setLyricNotFound(true);
|
||||
});
|
||||
return () => {
|
||||
retryButton.removeEventListener("clicked", handleBtnClick);
|
||||
dialog.hide();
|
||||
};
|
||||
}, [open, track, lyrics]);
|
||||
|
||||
useEffect(() => {
|
||||
retryButton.setEnabled(!lyricNotFound);
|
||||
}, [lyricNotFound]);
|
||||
useEffect(() => {
|
||||
retryButton.setEnabled(!lyricNotFound);
|
||||
}, [lyricNotFound]);
|
||||
|
||||
return <></>;
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default ManualLyricDialog;
|
||||
|
@ -1,12 +1,31 @@
|
||||
import { Direction, Orientation, QAbstractSliderSignals, QIcon } from "@nodegui/nodegui";
|
||||
import { BoxView, GridColumn, GridRow, GridView, Slider, Text, useEventHandler } from "@nodegui/react-nodegui";
|
||||
import {
|
||||
BoxView,
|
||||
GridColumn,
|
||||
GridRow,
|
||||
GridView,
|
||||
Slider,
|
||||
Text,
|
||||
useEventHandler,
|
||||
} from "@nodegui/react-nodegui";
|
||||
import React, { ReactElement, useContext, useEffect, useState } from "react";
|
||||
import playerContext, { CurrentPlaylist } from "../context/playerContext";
|
||||
import { shuffleArray } from "../helpers/shuffleArray";
|
||||
import NodeMpv from "node-mpv";
|
||||
import { getYoutubeTrack, YoutubeTrack } from "../helpers/getYoutubeTrack";
|
||||
import PlayerProgressBar from "./PlayerProgressBar";
|
||||
import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegular, heart, musicNode, download } from "../icons";
|
||||
import {
|
||||
random as shuffleIcon,
|
||||
play,
|
||||
pause,
|
||||
backward,
|
||||
forward,
|
||||
stop,
|
||||
heartRegular,
|
||||
heart,
|
||||
musicNode,
|
||||
download,
|
||||
} from "../icons";
|
||||
import IconButton from "./shared/IconButton";
|
||||
import showError from "../helpers/showError";
|
||||
import useTrackReaction from "../hooks/useTrackReaction";
|
||||
@ -15,259 +34,347 @@ import { LocalStorageKeys } from "../conf";
|
||||
import useDownloadQueue from "../hooks/useDownloadQueue";
|
||||
|
||||
export const audioPlayer = new NodeMpv(
|
||||
{
|
||||
audio_only: true,
|
||||
auto_restart: true,
|
||||
time_update: 1,
|
||||
binary: process.env.MPV_EXECUTABLE,
|
||||
// debug: true,
|
||||
// verbose: true,
|
||||
},
|
||||
["--ytdl-raw-options-set=format=140,http-chunk-size=300000"]
|
||||
{
|
||||
audio_only: true,
|
||||
auto_restart: true,
|
||||
time_update: 1,
|
||||
binary: process.env.MPV_EXECUTABLE,
|
||||
// debug: true,
|
||||
// verbose: true,
|
||||
},
|
||||
["--ytdl-raw-options-set=format=140,http-chunk-size=300000"],
|
||||
);
|
||||
function Player(): ReactElement {
|
||||
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
|
||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } =
|
||||
useContext(playerContext);
|
||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||
|
||||
const cachedVolume = localStorage.getItem(LocalStorageKeys.volume);
|
||||
const cachedVolume = localStorage.getItem(LocalStorageKeys.volume);
|
||||
|
||||
const [isPaused, setIsPaused] = useState(true);
|
||||
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"]>([]);
|
||||
const [isStopped, setIsStopped] = useState<boolean>(false);
|
||||
const [openLyrics, setOpenLyrics] = useState<boolean>(false);
|
||||
const [currentYtTrack, setCurrentYtTrack] = useState<YoutubeTrack>();
|
||||
const { addToQueue, isActiveDownloading, isFinishedDownloading } = useDownloadQueue();
|
||||
const [isPaused, setIsPaused] = useState(true);
|
||||
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"]>([]);
|
||||
const [isStopped, setIsStopped] = useState<boolean>(false);
|
||||
const [openLyrics, setOpenLyrics] = useState<boolean>(false);
|
||||
const [currentYtTrack, setCurrentYtTrack] = useState<YoutubeTrack>();
|
||||
const { addToQueue, isActiveDownloading, isFinishedDownloading } = useDownloadQueue();
|
||||
|
||||
const playlistTracksIds = currentPlaylist?.tracks?.map((t) => t.track.id) ?? [];
|
||||
const volumeHandler = useEventHandler<QAbstractSliderSignals>(
|
||||
{
|
||||
sliderMoved: (value) => {
|
||||
setVolume(value);
|
||||
},
|
||||
sliderReleased: () => {
|
||||
localStorage.setItem(LocalStorageKeys.volume, volume.toString());
|
||||
},
|
||||
},
|
||||
[volume]
|
||||
);
|
||||
const playerRunning = audioPlayer.isRunning();
|
||||
const cachedPlaylist = localStorage.getItem(LocalStorageKeys.cachedPlaylist);
|
||||
const cachedTrack = localStorage.getItem(LocalStorageKeys.cachedTrack);
|
||||
const playlistTracksIds = currentPlaylist?.tracks?.map((t) => t.track.id) ?? [];
|
||||
const volumeHandler = useEventHandler<QAbstractSliderSignals>(
|
||||
{
|
||||
sliderMoved: (value) => {
|
||||
setVolume(value);
|
||||
},
|
||||
sliderReleased: () => {
|
||||
localStorage.setItem(LocalStorageKeys.volume, volume.toString());
|
||||
},
|
||||
},
|
||||
[volume],
|
||||
);
|
||||
const playerRunning = audioPlayer.isRunning();
|
||||
const cachedPlaylist = localStorage.getItem(LocalStorageKeys.cachedPlaylist);
|
||||
const cachedTrack = localStorage.getItem(LocalStorageKeys.cachedTrack);
|
||||
|
||||
// initial Effect
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!playerRunning) {
|
||||
await audioPlayer.start();
|
||||
await audioPlayer.volume(volume);
|
||||
// initial Effect
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!playerRunning) {
|
||||
await audioPlayer.start();
|
||||
await audioPlayer.volume(volume);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, "[Failed starting audio player]: ");
|
||||
}
|
||||
})().then(() => {
|
||||
if (cachedPlaylist && !currentPlaylist) {
|
||||
setCurrentPlaylist(JSON.parse(cachedPlaylist));
|
||||
}
|
||||
if (cachedTrack && !currentTrack) {
|
||||
setCurrentTrack(JSON.parse(cachedTrack));
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (playerRunning) {
|
||||
audioPlayer.quit().catch((e: unknown) => console.log(e));
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// track change effect
|
||||
useEffect(() => {
|
||||
// caching current track
|
||||
if (!currentTrack) localStorage.removeItem(LocalStorageKeys.cachedTrack);
|
||||
else
|
||||
localStorage.setItem(
|
||||
LocalStorageKeys.cachedTrack,
|
||||
JSON.stringify(currentTrack),
|
||||
);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
if (currentTrack && playerRunning) {
|
||||
const youtubeTrack = await getYoutubeTrack(currentTrack);
|
||||
setCurrentYtTrack(youtubeTrack);
|
||||
await audioPlayer.load(youtubeTrack.youtube_uri, "replace");
|
||||
await audioPlayer.play();
|
||||
setIsPaused(false);
|
||||
}
|
||||
setIsStopped(false);
|
||||
} catch (error) {
|
||||
if (error.errcode !== 5) {
|
||||
setIsStopped(true);
|
||||
setIsPaused(true);
|
||||
}
|
||||
showError(error, "[Failure at track change]: ");
|
||||
}
|
||||
})();
|
||||
}, [currentTrack]);
|
||||
|
||||
// changing shuffle to default
|
||||
useEffect(() => {
|
||||
setShuffle(false);
|
||||
// caching playlist
|
||||
if (!currentPlaylist) localStorage.removeItem(LocalStorageKeys.cachedPlaylist);
|
||||
else
|
||||
localStorage.setItem(
|
||||
LocalStorageKeys.cachedPlaylist,
|
||||
JSON.stringify(currentPlaylist),
|
||||
);
|
||||
}, [currentPlaylist]);
|
||||
|
||||
useEffect(() => {
|
||||
if (playerRunning) {
|
||||
audioPlayer.volume(volume);
|
||||
}
|
||||
}, [volume]);
|
||||
|
||||
// for monitoring shuffle playlist
|
||||
useEffect(() => {
|
||||
if (currentPlaylist) {
|
||||
if (shuffle && realPlaylist.length === 0) {
|
||||
const shuffledTracks = shuffleArray(currentPlaylist.tracks);
|
||||
setRealPlaylist(currentPlaylist.tracks);
|
||||
setCurrentPlaylist({ ...currentPlaylist, tracks: shuffledTracks });
|
||||
} else if (!shuffle && realPlaylist.length > 0) {
|
||||
setCurrentPlaylist({ ...currentPlaylist, tracks: realPlaylist });
|
||||
}
|
||||
}
|
||||
}, [shuffle]);
|
||||
|
||||
// live Effect
|
||||
useEffect(() => {
|
||||
if (playerRunning) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const statusListener = (status: { property: string; value: any }) => {
|
||||
if (status?.property === "duration") {
|
||||
setTotalDuration(status.value ?? 0);
|
||||
}
|
||||
};
|
||||
const stopListener = () => {
|
||||
setIsStopped(true);
|
||||
setIsPaused(true);
|
||||
// go to next track
|
||||
if (
|
||||
currentTrack &&
|
||||
playlistTracksIds &&
|
||||
currentPlaylist?.tracks.length !== 0
|
||||
) {
|
||||
const index = playlistTracksIds?.indexOf(currentTrack.id) + 1;
|
||||
setCurrentTrack(
|
||||
currentPlaylist?.tracks[
|
||||
index > playlistTracksIds.length - 1 ? 0 : index
|
||||
].track,
|
||||
);
|
||||
}
|
||||
};
|
||||
const pauseListener = () => {
|
||||
setIsPaused(true);
|
||||
};
|
||||
const resumeListener = () => {
|
||||
setIsPaused(false);
|
||||
};
|
||||
audioPlayer.on("status", statusListener);
|
||||
audioPlayer.on("stopped", stopListener);
|
||||
audioPlayer.on("paused", pauseListener);
|
||||
audioPlayer.on("resumed", resumeListener);
|
||||
return () => {
|
||||
audioPlayer.off("status", statusListener);
|
||||
audioPlayer.off("stopped", stopListener);
|
||||
audioPlayer.off("paused", pauseListener);
|
||||
audioPlayer.off("resumed", resumeListener);
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, "[Failed starting audio player]: ");
|
||||
}
|
||||
})().then(() => {
|
||||
if (cachedPlaylist && !currentPlaylist) {
|
||||
setCurrentPlaylist(JSON.parse(cachedPlaylist));
|
||||
}
|
||||
if (cachedTrack && !currentTrack) {
|
||||
setCurrentTrack(JSON.parse(cachedTrack));
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (playerRunning) {
|
||||
audioPlayer.quit().catch((e: unknown) => console.log(e));
|
||||
}
|
||||
const handlePlayPause = async () => {
|
||||
try {
|
||||
if ((await audioPlayer.isPaused()) && playerRunning) {
|
||||
await audioPlayer.play();
|
||||
setIsStopped(false);
|
||||
setIsPaused(false);
|
||||
} else {
|
||||
await audioPlayer.pause();
|
||||
setIsStopped(true);
|
||||
setIsPaused(true);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, "[Track control failed]: ");
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// track change effect
|
||||
useEffect(() => {
|
||||
// caching current track
|
||||
if(!currentTrack) localStorage.removeItem(LocalStorageKeys.cachedTrack);
|
||||
else localStorage.setItem(LocalStorageKeys.cachedTrack, JSON.stringify(currentTrack));
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
if (currentTrack && playerRunning) {
|
||||
const youtubeTrack = await getYoutubeTrack(currentTrack);
|
||||
setCurrentYtTrack(youtubeTrack);
|
||||
await audioPlayer.load(youtubeTrack.youtube_uri, "replace");
|
||||
await audioPlayer.play();
|
||||
setIsPaused(false);
|
||||
const prevOrNext = (constant: number) => {
|
||||
if (currentTrack && playlistTracksIds && currentPlaylist) {
|
||||
const index = playlistTracksIds.indexOf(currentTrack.id) + constant;
|
||||
setCurrentTrack(
|
||||
currentPlaylist.tracks[
|
||||
index > playlistTracksIds?.length - 1
|
||||
? 0
|
||||
: index < 0
|
||||
? playlistTracksIds.length - 1
|
||||
: index
|
||||
].track,
|
||||
);
|
||||
}
|
||||
setIsStopped(false);
|
||||
} catch (error) {
|
||||
if (error.errcode !== 5) {
|
||||
setIsStopped(true);
|
||||
setIsPaused(true);
|
||||
};
|
||||
|
||||
async function stopPlayback() {
|
||||
try {
|
||||
if (playerRunning) {
|
||||
setCurrentTrack(undefined);
|
||||
setCurrentPlaylist(undefined);
|
||||
await audioPlayer.stop();
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, "[Failed at audio-player stop]: ");
|
||||
}
|
||||
showError(error, "[Failure at track change]: ");
|
||||
}
|
||||
})();
|
||||
}, [currentTrack]);
|
||||
|
||||
// changing shuffle to default
|
||||
useEffect(() => {
|
||||
setShuffle(false);
|
||||
// caching playlist
|
||||
if(!currentPlaylist) localStorage.removeItem(LocalStorageKeys.cachedPlaylist);
|
||||
else localStorage.setItem(LocalStorageKeys.cachedPlaylist, JSON.stringify(currentPlaylist));
|
||||
}, [currentPlaylist]);
|
||||
|
||||
useEffect(() => {
|
||||
if (playerRunning) {
|
||||
audioPlayer.volume(volume);
|
||||
}
|
||||
}, [volume]);
|
||||
|
||||
// for monitoring shuffle playlist
|
||||
useEffect(() => {
|
||||
if (currentPlaylist) {
|
||||
if (shuffle && realPlaylist.length === 0) {
|
||||
const shuffledTracks = shuffleArray(currentPlaylist.tracks);
|
||||
setRealPlaylist(currentPlaylist.tracks);
|
||||
setCurrentPlaylist({ ...currentPlaylist, tracks: shuffledTracks });
|
||||
} else if (!shuffle && realPlaylist.length > 0) {
|
||||
setCurrentPlaylist({ ...currentPlaylist, tracks: realPlaylist });
|
||||
}
|
||||
}
|
||||
}, [shuffle]);
|
||||
|
||||
// live Effect
|
||||
useEffect(() => {
|
||||
if (playerRunning) {
|
||||
const statusListener = (status: { property: string; value: any }) => {
|
||||
if (status?.property === "duration") {
|
||||
setTotalDuration(status.value ?? 0);
|
||||
}
|
||||
};
|
||||
const stopListener = () => {
|
||||
setIsStopped(true);
|
||||
setIsPaused(true);
|
||||
// go to next track
|
||||
if (currentTrack && playlistTracksIds && currentPlaylist?.tracks.length !== 0) {
|
||||
const index = playlistTracksIds?.indexOf(currentTrack.id) + 1;
|
||||
setCurrentTrack(currentPlaylist?.tracks[index > playlistTracksIds.length - 1 ? 0 : index].track);
|
||||
}
|
||||
};
|
||||
const pauseListener = () => {
|
||||
setIsPaused(true);
|
||||
};
|
||||
const resumeListener = () => {
|
||||
setIsPaused(false);
|
||||
};
|
||||
audioPlayer.on("status", statusListener);
|
||||
audioPlayer.on("stopped", stopListener);
|
||||
audioPlayer.on("paused", pauseListener);
|
||||
audioPlayer.on("resumed", resumeListener);
|
||||
return () => {
|
||||
audioPlayer.off("status", statusListener);
|
||||
audioPlayer.off("stopped", stopListener);
|
||||
audioPlayer.off("paused", pauseListener);
|
||||
audioPlayer.off("resumed", resumeListener);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const handlePlayPause = async () => {
|
||||
try {
|
||||
if ((await audioPlayer.isPaused()) && playerRunning) {
|
||||
await audioPlayer.play();
|
||||
setIsStopped(false);
|
||||
setIsPaused(false);
|
||||
} else {
|
||||
await audioPlayer.pause();
|
||||
setIsStopped(true);
|
||||
setIsPaused(true);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, "[Track control failed]: ");
|
||||
}
|
||||
};
|
||||
|
||||
const prevOrNext = (constant: number) => {
|
||||
if (currentTrack && playlistTracksIds && currentPlaylist) {
|
||||
const index = playlistTracksIds.indexOf(currentTrack.id) + constant;
|
||||
setCurrentTrack(currentPlaylist.tracks[index > playlistTracksIds?.length - 1 ? 0 : index < 0 ? playlistTracksIds.length - 1 : index].track);
|
||||
}
|
||||
};
|
||||
|
||||
async function stopPlayback() {
|
||||
try {
|
||||
if (playerRunning) {
|
||||
setCurrentTrack(undefined);
|
||||
setCurrentPlaylist(undefined);
|
||||
await audioPlayer.stop();
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, "[Failed at audio-player stop]: ");
|
||||
}
|
||||
}
|
||||
|
||||
const artistsNames = currentTrack?.artists?.map((x) => x.name);
|
||||
return (
|
||||
<GridView enabled={!!currentTrack} style="flex: 1; max-height: 120px;">
|
||||
<GridRow>
|
||||
<GridColumn width={2}>
|
||||
<Text wordWrap openExternalLinks>
|
||||
{artistsNames && currentTrack
|
||||
? `
|
||||
<p><b><a href="${currentYtTrack?.youtube_uri}"}>${currentTrack.name}</a></b> - ${artistsNames[0]} ${artistsNames.length > 1 ? "feat. " + artistsNames.slice(1).join(", ") : ""}</p>
|
||||
const artistsNames = currentTrack?.artists?.map((x) => x.name);
|
||||
return (
|
||||
<GridView enabled={!!currentTrack} style="flex: 1; max-height: 120px;">
|
||||
<GridRow>
|
||||
<GridColumn width={2}>
|
||||
<Text wordWrap openExternalLinks>
|
||||
{artistsNames && currentTrack
|
||||
? `
|
||||
<p><b><a href="${currentYtTrack?.youtube_uri}"}>${
|
||||
currentTrack.name
|
||||
}</a></b> - ${artistsNames[0]} ${
|
||||
artistsNames.length > 1
|
||||
? "feat. " + artistsNames.slice(1).join(", ")
|
||||
: ""
|
||||
}</p>
|
||||
`
|
||||
: `<b>Oh, dear don't waste time</b>`}
|
||||
</Text>
|
||||
</GridColumn>
|
||||
<GridColumn width={4}>
|
||||
<BoxView direction={Direction.TopToBottom} style={`max-width: 600px; min-width: 380px;`}>
|
||||
{currentTrack && <ManualLyricDialog open={openLyrics} track={currentTrack} />}
|
||||
<PlayerProgressBar audioPlayer={audioPlayer} totalDuration={totalDuration} />
|
||||
: `<b>Oh, dear don't waste time</b>`}
|
||||
</Text>
|
||||
</GridColumn>
|
||||
<GridColumn width={4}>
|
||||
<BoxView
|
||||
direction={Direction.TopToBottom}
|
||||
style={`max-width: 600px; min-width: 380px;`}
|
||||
>
|
||||
{currentTrack && (
|
||||
<ManualLyricDialog open={openLyrics} track={currentTrack} />
|
||||
)}
|
||||
<PlayerProgressBar
|
||||
audioPlayer={audioPlayer}
|
||||
totalDuration={totalDuration}
|
||||
/>
|
||||
|
||||
<BoxView direction={Direction.LeftToRight}>
|
||||
<IconButton style={`background-color: ${shuffle ? "orange" : "rgba(255, 255, 255, 0.055)"}`} on={{ clicked: () => setShuffle(!shuffle) }} icon={new QIcon(shuffleIcon)} />
|
||||
<IconButton on={{ clicked: () => prevOrNext(-1) }} icon={new QIcon(backward)} />
|
||||
<IconButton on={{ clicked: handlePlayPause }} icon={new QIcon(isStopped || isPaused || !currentTrack ? play : pause)} />
|
||||
<IconButton on={{ clicked: () => prevOrNext(1) }} icon={new QIcon(forward)} />
|
||||
<IconButton icon={new QIcon(stop)} on={{ clicked: stopPlayback }} />
|
||||
</BoxView>
|
||||
</BoxView>
|
||||
</GridColumn>
|
||||
<GridColumn width={2}>
|
||||
<BoxView>
|
||||
<IconButton
|
||||
style={isActiveDownloading() && !isFinishedDownloading() ? "background-color: green;" : ""}
|
||||
enabled={!!currentYtTrack}
|
||||
icon={new QIcon(download)}
|
||||
on={{
|
||||
clicked() {
|
||||
currentYtTrack && addToQueue(currentYtTrack);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
on={{
|
||||
clicked() {
|
||||
if (currentTrack) {
|
||||
reactToTrack({ added_at: Date.now().toString(), track: currentTrack });
|
||||
}
|
||||
},
|
||||
}}
|
||||
icon={new QIcon(isFavorite(currentTrack?.id ?? "") ? heart : heartRegular)}
|
||||
/>
|
||||
<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>
|
||||
</GridRow>
|
||||
</GridView>
|
||||
);
|
||||
<BoxView direction={Direction.LeftToRight}>
|
||||
<IconButton
|
||||
style={`background-color: ${
|
||||
shuffle ? "orange" : "rgba(255, 255, 255, 0.055)"
|
||||
}`}
|
||||
on={{ clicked: () => setShuffle(!shuffle) }}
|
||||
icon={new QIcon(shuffleIcon)}
|
||||
/>
|
||||
<IconButton
|
||||
on={{ clicked: () => prevOrNext(-1) }}
|
||||
icon={new QIcon(backward)}
|
||||
/>
|
||||
<IconButton
|
||||
on={{ clicked: handlePlayPause }}
|
||||
icon={
|
||||
new QIcon(
|
||||
isStopped || isPaused || !currentTrack
|
||||
? play
|
||||
: pause,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
on={{ clicked: () => prevOrNext(1) }}
|
||||
icon={new QIcon(forward)}
|
||||
/>
|
||||
<IconButton
|
||||
icon={new QIcon(stop)}
|
||||
on={{ clicked: stopPlayback }}
|
||||
/>
|
||||
</BoxView>
|
||||
</BoxView>
|
||||
</GridColumn>
|
||||
<GridColumn width={2}>
|
||||
<BoxView>
|
||||
<IconButton
|
||||
style={
|
||||
isActiveDownloading() && !isFinishedDownloading()
|
||||
? "background-color: green;"
|
||||
: ""
|
||||
}
|
||||
enabled={!!currentYtTrack}
|
||||
icon={new QIcon(download)}
|
||||
on={{
|
||||
clicked() {
|
||||
currentYtTrack && addToQueue(currentYtTrack);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
on={{
|
||||
clicked() {
|
||||
if (currentTrack) {
|
||||
reactToTrack({
|
||||
added_at: Date.now().toString(),
|
||||
track: currentTrack,
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
icon={
|
||||
new QIcon(
|
||||
isFavorite(currentTrack?.id ?? "")
|
||||
? heart
|
||||
: heartRegular,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
</GridRow>
|
||||
</GridView>
|
||||
);
|
||||
}
|
||||
|
||||
export default Player;
|
||||
|
@ -5,58 +5,70 @@ import React, { useContext, useEffect, useState } from "react";
|
||||
import playerContext from "../context/playerContext";
|
||||
|
||||
interface PlayerProgressBarProps {
|
||||
audioPlayer: NodeMpv;
|
||||
totalDuration: number;
|
||||
audioPlayer: NodeMpv;
|
||||
totalDuration: number;
|
||||
}
|
||||
|
||||
function PlayerProgressBar({ audioPlayer, totalDuration }: PlayerProgressBarProps) {
|
||||
const { currentTrack } = useContext(playerContext);
|
||||
const [trackTime, setTrackTime] = useState<number>(0);
|
||||
const trackSliderEvents = useEventHandler<QAbstractSliderSignals>(
|
||||
{
|
||||
sliderMoved: (value) => {
|
||||
if (audioPlayer.isRunning() && currentTrack) {
|
||||
const newPosition = (totalDuration * value) / 100;
|
||||
setTrackTime(parseInt(newPosition.toString()));
|
||||
}
|
||||
},
|
||||
sliderReleased: () => {
|
||||
(async () => {
|
||||
try {
|
||||
await audioPlayer.goToPosition(trackTime);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
})();
|
||||
},
|
||||
},
|
||||
[currentTrack, totalDuration, trackTime]
|
||||
);
|
||||
const { currentTrack } = useContext(playerContext);
|
||||
const [trackTime, setTrackTime] = useState<number>(0);
|
||||
const trackSliderEvents = useEventHandler<QAbstractSliderSignals>(
|
||||
{
|
||||
sliderMoved: (value) => {
|
||||
if (audioPlayer.isRunning() && currentTrack) {
|
||||
const newPosition = (totalDuration * value) / 100;
|
||||
setTrackTime(parseInt(newPosition.toString()));
|
||||
}
|
||||
},
|
||||
sliderReleased: () => {
|
||||
(async () => {
|
||||
try {
|
||||
await audioPlayer.goToPosition(trackTime);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
})();
|
||||
},
|
||||
},
|
||||
[currentTrack, totalDuration, trackTime],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const progressListener = (seconds: number) => {
|
||||
setTrackTime(seconds);
|
||||
};
|
||||
const statusListener = ({ property }: import("node-mpv").StatusObject): void => {
|
||||
if (property === "filename") {
|
||||
setTrackTime(0);
|
||||
}
|
||||
};
|
||||
audioPlayer.on("status", statusListener);
|
||||
audioPlayer.on("timeposition", progressListener);
|
||||
return () => {
|
||||
audioPlayer.off("status", statusListener);
|
||||
audioPlayer.off("timeposition", progressListener);
|
||||
};
|
||||
});
|
||||
const playbackPercentage = totalDuration > 0 ? (trackTime * 100) / totalDuration : 0;
|
||||
const playbackTime = new Date(trackTime * 1000).toISOString().substr(14, 5) + "/" + new Date(totalDuration * 1000).toISOString().substr(14, 5)
|
||||
return (
|
||||
<BoxView direction={Direction.LeftToRight} style={`padding: 20px 0px; flex-direction: row;`}>
|
||||
<Slider enabled={!!currentTrack || trackTime > 0} on={trackSliderEvents} sliderPosition={playbackPercentage} hasTracking orientation={Orientation.Horizontal} />
|
||||
<Text>{playbackTime}</Text>
|
||||
</BoxView>
|
||||
);
|
||||
useEffect(() => {
|
||||
const progressListener = (seconds: number) => {
|
||||
setTrackTime(seconds);
|
||||
};
|
||||
const statusListener = ({ property }: import("node-mpv").StatusObject): void => {
|
||||
if (property === "filename") {
|
||||
setTrackTime(0);
|
||||
}
|
||||
};
|
||||
audioPlayer.on("status", statusListener);
|
||||
audioPlayer.on("timeposition", progressListener);
|
||||
return () => {
|
||||
audioPlayer.off("status", statusListener);
|
||||
audioPlayer.off("timeposition", progressListener);
|
||||
};
|
||||
});
|
||||
const playbackPercentage = totalDuration > 0 ? (trackTime * 100) / totalDuration : 0;
|
||||
const playbackTime =
|
||||
new Date(trackTime * 1000).toISOString().substr(14, 5) +
|
||||
"/" +
|
||||
new Date(totalDuration * 1000).toISOString().substr(14, 5);
|
||||
return (
|
||||
<BoxView
|
||||
direction={Direction.LeftToRight}
|
||||
style={`padding: 20px 0px; flex-direction: row;`}
|
||||
>
|
||||
<Slider
|
||||
enabled={!!currentTrack || trackTime > 0}
|
||||
on={trackSliderEvents}
|
||||
sliderPosition={playbackPercentage}
|
||||
hasTracking
|
||||
orientation={Orientation.Horizontal}
|
||||
/>
|
||||
<Text>{playbackTime}</Text>
|
||||
</BoxView>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlayerProgressBar;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { QAbstractButtonSignals } from "@nodegui/nodegui";
|
||||
import { Button, ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||
import React from "react";
|
||||
import { RefetchOptions } from "react-query";
|
||||
import { useLocation, useParams } from "react-router";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
||||
@ -9,61 +10,79 @@ import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||
import PlaylistCard from "./shared/PlaylistCard";
|
||||
|
||||
function PlaylistGenreView() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const location = useLocation<{ name: string }>();
|
||||
const { data: pagedPlaylists, isError, isLoading, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = useSpotifyInfiniteQuery<SpotifyApi.PagingObject<SpotifyApi.PlaylistObjectSimplified>>(
|
||||
[QueryCacheKeys.genrePlaylists, id],
|
||||
async (spotifyApi, { pageParam }) => {
|
||||
const option = { limit: 20, offset: pageParam };
|
||||
let res;
|
||||
if (id === "featured") {
|
||||
res = await spotifyApi.getFeaturedPlaylists(option);
|
||||
} else {
|
||||
res = await spotifyApi.getPlaylistsForCategory(id, option);
|
||||
}
|
||||
return res.body.playlists;
|
||||
},
|
||||
{
|
||||
getNextPageParam(lastPage) {
|
||||
if (lastPage.next) {
|
||||
return lastPage.offset + lastPage.limit;
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const location = useLocation<{ name: string }>();
|
||||
const {
|
||||
data: pagedPlaylists,
|
||||
isError,
|
||||
isLoading,
|
||||
refetch,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
} = useSpotifyInfiniteQuery<
|
||||
SpotifyApi.PagingObject<SpotifyApi.PlaylistObjectSimplified>
|
||||
>(
|
||||
[QueryCacheKeys.genrePlaylists, id],
|
||||
async (spotifyApi, { pageParam }) => {
|
||||
const option = { limit: 20, offset: pageParam };
|
||||
let res;
|
||||
if (id === "featured") {
|
||||
res = await spotifyApi.getFeaturedPlaylists(option);
|
||||
} else {
|
||||
res = await spotifyApi.getPlaylistsForCategory(id, option);
|
||||
}
|
||||
return res.body.playlists;
|
||||
},
|
||||
{
|
||||
getNextPageParam(lastPage) {
|
||||
if (lastPage.next) {
|
||||
return lastPage.offset + lastPage.limit;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const playlists = pagedPlaylists?.pages
|
||||
.map((page) => page.items)
|
||||
.filter(Boolean)
|
||||
.flat(1);
|
||||
const playlists = pagedPlaylists?.pages
|
||||
.map((page) => page.items)
|
||||
.filter(Boolean)
|
||||
.flat(1);
|
||||
|
||||
return (
|
||||
<GenreView
|
||||
isError={isError}
|
||||
isLoading={isLoading || isFetchingNextPage}
|
||||
refetch={refetch}
|
||||
heading={location.state.name}
|
||||
playlists={playlists ?? []}
|
||||
loadMore={hasNextPage ? () => fetchNextPage() : undefined}
|
||||
isLoadable={!isFetchingNextPage}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<GenreView
|
||||
isError={isError}
|
||||
isLoading={isLoading || isFetchingNextPage}
|
||||
refetch={refetch}
|
||||
heading={location.state.name}
|
||||
playlists={playlists ?? []}
|
||||
loadMore={hasNextPage ? () => fetchNextPage() : undefined}
|
||||
isLoadable={!isFetchingNextPage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlaylistGenreView;
|
||||
|
||||
interface GenreViewProps {
|
||||
heading: string;
|
||||
playlists: SpotifyApi.PlaylistObjectSimplified[];
|
||||
loadMore?: QAbstractButtonSignals["clicked"];
|
||||
isLoadable?: boolean;
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
refetch: Function;
|
||||
heading: string;
|
||||
playlists: SpotifyApi.PlaylistObjectSimplified[];
|
||||
loadMore?: QAbstractButtonSignals["clicked"];
|
||||
isLoadable?: boolean;
|
||||
isError: boolean;
|
||||
isLoading: boolean;
|
||||
refetch: (options?: RefetchOptions | undefined) => Promise<unknown>;
|
||||
}
|
||||
|
||||
export function GenreView({ heading, playlists, loadMore, isLoadable, isError, isLoading, refetch }: GenreViewProps) {
|
||||
const playlistGenreViewStylesheet = `
|
||||
export function GenreView({
|
||||
heading,
|
||||
playlists,
|
||||
loadMore,
|
||||
isLoadable,
|
||||
isError,
|
||||
isLoading,
|
||||
refetch,
|
||||
}: GenreViewProps) {
|
||||
const playlistGenreViewStylesheet = `
|
||||
#genre-container{
|
||||
flex-direction: 'column';
|
||||
flex: 1;
|
||||
@ -83,19 +102,32 @@ export function GenreView({ heading, playlists, loadMore, isLoadable, isError, i
|
||||
width: 330px;
|
||||
}
|
||||
`;
|
||||
return (
|
||||
<View id="genre-container" styleSheet={playlistGenreViewStylesheet}>
|
||||
<BackButton />
|
||||
<Text id="heading">{`<h2>${heading}</h2>`}</Text>
|
||||
<ScrollArea id="scroll-view">
|
||||
<View id="child-container">
|
||||
<PlaceholderApplet error={isError} loading={isLoading} reload={refetch} message={`Failed loading ${heading}'s playlists`} />
|
||||
{playlists?.map((playlist, index) => {
|
||||
return <PlaylistCard key={index + playlist.id} playlist={playlist} />;
|
||||
})}
|
||||
{loadMore && <Button text="Load more" on={{ clicked: loadMore }} enabled={isLoadable} />}
|
||||
return (
|
||||
<View id="genre-container" styleSheet={playlistGenreViewStylesheet}>
|
||||
<BackButton />
|
||||
<Text id="heading">{`<h2>${heading}</h2>`}</Text>
|
||||
<ScrollArea id="scroll-view">
|
||||
<View id="child-container">
|
||||
<PlaceholderApplet
|
||||
error={isError}
|
||||
loading={isLoading}
|
||||
reload={refetch}
|
||||
message={`Failed loading ${heading}'s playlists`}
|
||||
/>
|
||||
{playlists?.map((playlist, index) => {
|
||||
return (
|
||||
<PlaylistCard key={index + playlist.id} playlist={playlist} />
|
||||
);
|
||||
})}
|
||||
{loadMore && (
|
||||
<Button
|
||||
text="Load more"
|
||||
on={{ clicked: loadMore }}
|
||||
enabled={isLoadable}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
@ -14,94 +14,130 @@ import { TrackButton } from "./shared/TrackButton";
|
||||
import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||
|
||||
export interface PlaylistTrackRes {
|
||||
name: string;
|
||||
artists: string;
|
||||
url: string;
|
||||
name: string;
|
||||
artists: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const PlaylistView: FC = () => {
|
||||
const { setCurrentTrack, currentPlaylist, setCurrentPlaylist } = useContext(playerContext);
|
||||
const params = useParams<{ id: string }>();
|
||||
const location = useLocation<{ name: string; thumbnail: string }>();
|
||||
const { isFavorite, reactToPlaylist } = usePlaylistReaction();
|
||||
const { data: playlist } = useSpotifyQuery<SpotifyApi.PlaylistObjectFull>([QueryCacheKeys.categoryPlaylists, params.id], (spotifyApi) =>
|
||||
spotifyApi.getPlaylist(params.id).then((playlistsRes) => playlistsRes.body)
|
||||
);
|
||||
const { data: tracks, isSuccess, isError, isLoading, refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>(
|
||||
[QueryCacheKeys.playlistTracks, params.id],
|
||||
(spotifyApi) => spotifyApi.getPlaylistTracks(params.id).then((track) => track.body.items),
|
||||
{ initialData: [] }
|
||||
);
|
||||
const { setCurrentTrack, currentPlaylist, setCurrentPlaylist } =
|
||||
useContext(playerContext);
|
||||
const params = useParams<{ id: string }>();
|
||||
const location = useLocation<{ name: string; thumbnail: string }>();
|
||||
const { isFavorite, reactToPlaylist } = usePlaylistReaction();
|
||||
const { data: playlist } = useSpotifyQuery<SpotifyApi.PlaylistObjectFull>(
|
||||
[QueryCacheKeys.categoryPlaylists, params.id],
|
||||
(spotifyApi) =>
|
||||
spotifyApi.getPlaylist(params.id).then((playlistsRes) => playlistsRes.body),
|
||||
);
|
||||
const {
|
||||
data: tracks,
|
||||
isSuccess,
|
||||
isError,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>(
|
||||
[QueryCacheKeys.playlistTracks, params.id],
|
||||
(spotifyApi) =>
|
||||
spotifyApi.getPlaylistTracks(params.id).then((track) => track.body.items),
|
||||
{ initialData: [] },
|
||||
);
|
||||
|
||||
const handlePlaylistPlayPause = () => {
|
||||
if (currentPlaylist?.id !== params.id && isSuccess && tracks) {
|
||||
setCurrentPlaylist({ ...params, ...location.state, tracks });
|
||||
setCurrentTrack(tracks[0].track);
|
||||
} else {
|
||||
audioPlayer.stop().catch((error) => console.error("Failed to stop audio player: ", error));
|
||||
setCurrentTrack(undefined);
|
||||
setCurrentPlaylist(undefined);
|
||||
}
|
||||
};
|
||||
const handlePlaylistPlayPause = () => {
|
||||
if (currentPlaylist?.id !== params.id && isSuccess && tracks) {
|
||||
setCurrentPlaylist({ ...params, ...location.state, tracks });
|
||||
setCurrentTrack(tracks[0].track);
|
||||
} else {
|
||||
audioPlayer
|
||||
.stop()
|
||||
.catch((error) => console.error("Failed to stop audio player: ", error));
|
||||
setCurrentTrack(undefined);
|
||||
setCurrentPlaylist(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={`flex: 1; flex-direction: 'column';`}>
|
||||
<PlaylistSimpleControls
|
||||
handlePlaylistReact={() => playlist && reactToPlaylist(playlist)}
|
||||
handlePlaylistPlayPause={handlePlaylistPlayPause}
|
||||
isActive={currentPlaylist?.id === params.id}
|
||||
isFavorite={isFavorite(params.id)}
|
||||
/>
|
||||
<Text>{`<center><h2>${location.state.name[0].toUpperCase()}${location.state.name.slice(1)}</h2></center>`}</Text>
|
||||
{<TrackTableIndex />}
|
||||
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
||||
<View style={`flex-direction:column; flex: 1;`}>
|
||||
<PlaceholderApplet error={isError} loading={isLoading} reload={refetch} message={`Failed retrieving ${location.state.name} tracks`} />
|
||||
{tracks?.map(({ track }, index) => {
|
||||
if (track) {
|
||||
return <TrackButton key={index + track.id} track={track} index={index} playlist={playlist} />;
|
||||
}
|
||||
})}
|
||||
return (
|
||||
<View style={`flex: 1; flex-direction: 'column';`}>
|
||||
<PlaylistSimpleControls
|
||||
handlePlaylistReact={() => playlist && reactToPlaylist(playlist)}
|
||||
handlePlaylistPlayPause={handlePlaylistPlayPause}
|
||||
isActive={currentPlaylist?.id === params.id}
|
||||
isFavorite={isFavorite(params.id)}
|
||||
/>
|
||||
<Text>{`<center><h2>${location.state.name[0].toUpperCase()}${location.state.name.slice(
|
||||
1,
|
||||
)}</h2></center>`}</Text>
|
||||
{<TrackTableIndex />}
|
||||
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
||||
<View style={`flex-direction:column; flex: 1;`}>
|
||||
<PlaceholderApplet
|
||||
error={isError}
|
||||
loading={isLoading}
|
||||
reload={refetch}
|
||||
message={`Failed retrieving ${location.state.name} tracks`}
|
||||
/>
|
||||
{tracks?.map(({ track }, index) => {
|
||||
if (track) {
|
||||
return (
|
||||
<TrackButton
|
||||
key={index + track.id}
|
||||
track={track}
|
||||
index={index}
|
||||
playlist={playlist}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaylistView;
|
||||
|
||||
export function TrackTableIndex() {
|
||||
return (
|
||||
<View>
|
||||
<Text style="padding: 5px;">{`<h3>#</h3>`}</Text>
|
||||
<Text style="width: '35%';">{`<h3>Title</h3>`}</Text>
|
||||
<Text style="width: '25%';">{`<h3>Album</h3>`}</Text>
|
||||
<Text style="width: '15%';">{`<h3>Duration</h3>`}</Text>
|
||||
<Text style="width: '15%';">{`<h3>Actions</h3>`}</Text>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View>
|
||||
<Text style="padding: 5px;">{`<h3>#</h3>`}</Text>
|
||||
<Text style="width: '35%';">{`<h3>Title</h3>`}</Text>
|
||||
<Text style="width: '25%';">{`<h3>Album</h3>`}</Text>
|
||||
<Text style="width: '15%';">{`<h3>Duration</h3>`}</Text>
|
||||
<Text style="width: '15%';">{`<h3>Actions</h3>`}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
interface PlaylistSimpleControlsProps {
|
||||
handlePlaylistPlayPause: (index?: number) => void;
|
||||
handlePlaylistReact?: () => void;
|
||||
isActive: boolean;
|
||||
isFavorite?: boolean;
|
||||
handlePlaylistPlayPause: (index?: number) => void;
|
||||
handlePlaylistReact?: () => void;
|
||||
isActive: boolean;
|
||||
isFavorite?: boolean;
|
||||
}
|
||||
|
||||
export function PlaylistSimpleControls({ handlePlaylistPlayPause, isActive, handlePlaylistReact, isFavorite }: PlaylistSimpleControlsProps) {
|
||||
return (
|
||||
<View style={`justify-content: 'space-evenly'; max-width: 150px; padding: 10px;`}>
|
||||
<BackButton />
|
||||
{isFavorite !== undefined && <IconButton icon={new QIcon(isFavorite ? heart : heartRegular)} on={{ clicked: handlePlaylistReact }} />}
|
||||
<IconButton
|
||||
style={`background-color: #00be5f; color: white;`}
|
||||
on={{
|
||||
clicked() {
|
||||
handlePlaylistPlayPause();
|
||||
},
|
||||
}}
|
||||
icon={new QIcon(isActive ? stop : play)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
export function PlaylistSimpleControls({
|
||||
handlePlaylistPlayPause,
|
||||
isActive,
|
||||
handlePlaylistReact,
|
||||
isFavorite,
|
||||
}: PlaylistSimpleControlsProps) {
|
||||
return (
|
||||
<View style={`justify-content: 'space-evenly'; max-width: 150px; padding: 10px;`}>
|
||||
<BackButton />
|
||||
{isFavorite !== undefined && (
|
||||
<IconButton
|
||||
icon={new QIcon(isFavorite ? heart : heartRegular)}
|
||||
on={{ clicked: handlePlaylistReact }}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
style={`background-color: #00be5f; color: white;`}
|
||||
on={{
|
||||
clicked() {
|
||||
handlePlaylistPlayPause();
|
||||
},
|
||||
}}
|
||||
icon={new QIcon(isActive ? stop : play)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -13,83 +13,122 @@ import PlaylistCard from "./shared/PlaylistCard";
|
||||
import { TrackButton } from "./shared/TrackButton";
|
||||
|
||||
function Search() {
|
||||
const history = useHistory<{ searchQuery: string }>();
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const { data: searchResults, refetch, isError, isLoading } = useSpotifyQuery<SpotifyApi.SearchResponse>(
|
||||
QueryCacheKeys.search,
|
||||
(spotifyApi) => spotifyApi.search(searchQuery, ["playlist", "track"], { limit: 4 }).then((res) => res.body),
|
||||
{ enabled: false }
|
||||
);
|
||||
const history = useHistory<{ searchQuery: string }>();
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const {
|
||||
data: searchResults,
|
||||
refetch,
|
||||
isError,
|
||||
isLoading,
|
||||
} = useSpotifyQuery<SpotifyApi.SearchResponse>(
|
||||
QueryCacheKeys.search,
|
||||
(spotifyApi) =>
|
||||
spotifyApi
|
||||
.search(searchQuery, ["playlist", "track"], { limit: 4 })
|
||||
.then((res) => res.body),
|
||||
{ enabled: false },
|
||||
);
|
||||
|
||||
async function handleSearch() {
|
||||
try {
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
showError(error, "[Failed to search through spotify]: ");
|
||||
async function handleSearch() {
|
||||
try {
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
showError(error, "[Failed to search through spotify]: ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const placeholder = <PlaceholderApplet error={isError} loading={isLoading} message="Failed querying spotify" reload={refetch} />;
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column'; padding: 5px;">
|
||||
<View>
|
||||
<LineEdit
|
||||
style="width: '65%'; margin: 5px;"
|
||||
placeholderText="Search spotify"
|
||||
on={{
|
||||
textChanged(t) {
|
||||
setSearchQuery(t);
|
||||
},
|
||||
KeyRelease(native: any) {
|
||||
const key = new QKeyEvent(native);
|
||||
const isEnter = key.key() === 16777220;
|
||||
if (isEnter) {
|
||||
handleSearch();
|
||||
}
|
||||
}
|
||||
}}
|
||||
const placeholder = (
|
||||
<PlaceholderApplet
|
||||
error={isError}
|
||||
loading={isLoading}
|
||||
message="Failed querying spotify"
|
||||
reload={refetch}
|
||||
/>
|
||||
<IconButton enabled={searchQuery.length > 0} icon={new QIcon(search)} on={{ clicked: handleSearch }} />
|
||||
</View>
|
||||
<ScrollArea style="flex: 1;">
|
||||
<View style="flex-direction: 'column'; flex: 1;">
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
on={{
|
||||
MouseButtonRelease(native: any) {
|
||||
if (new QMouseEvent(native).button() === 1 && searchResults?.tracks) {
|
||||
history.push("/search/songs", { searchQuery });
|
||||
}
|
||||
},
|
||||
}}>{`<h2>Songs</h2>`}</Text>
|
||||
<TrackTableIndex />
|
||||
{placeholder}
|
||||
{searchResults?.tracks?.items.map((track, index) => (
|
||||
<TrackButton key={index + track.id} index={index} track={track} />
|
||||
))}
|
||||
</View>
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
on={{
|
||||
MouseButtonRelease(native: any) {
|
||||
if (new QMouseEvent(native).button() === 1 && searchResults?.playlists) {
|
||||
history.push("/search/playlists", { searchQuery });
|
||||
}
|
||||
},
|
||||
}}>{`<h2>Playlists</h2>`}</Text>
|
||||
<View style="flex: 1; justify-content: 'space-around'; align-items: 'center';">
|
||||
{placeholder}
|
||||
{searchResults?.playlists?.items.map((playlist, index) => (
|
||||
<PlaylistCard key={index + playlist.id} playlist={playlist} />
|
||||
))}
|
||||
);
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column'; padding: 5px;">
|
||||
<View>
|
||||
<LineEdit
|
||||
style="width: '65%'; margin: 5px;"
|
||||
placeholderText="Search spotify"
|
||||
on={{
|
||||
textChanged(t) {
|
||||
setSearchQuery(t);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
KeyRelease(native: any) {
|
||||
const key = new QKeyEvent(native);
|
||||
const isEnter = key.key() === 16777220;
|
||||
if (isEnter) {
|
||||
handleSearch();
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
enabled={searchQuery.length > 0}
|
||||
icon={new QIcon(search)}
|
||||
on={{ clicked: handleSearch }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<ScrollArea style="flex: 1;">
|
||||
<View style="flex-direction: 'column'; flex: 1;">
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
on={{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
MouseButtonRelease(native: any) {
|
||||
if (
|
||||
new QMouseEvent(native).button() === 1 &&
|
||||
searchResults?.tracks
|
||||
) {
|
||||
history.push("/search/songs", { searchQuery });
|
||||
}
|
||||
},
|
||||
}}
|
||||
>{`<h2>Songs</h2>`}</Text>
|
||||
<TrackTableIndex />
|
||||
{placeholder}
|
||||
{searchResults?.tracks?.items.map((track, index) => (
|
||||
<TrackButton
|
||||
key={index + track.id}
|
||||
index={index}
|
||||
track={track}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
on={{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
MouseButtonRelease(native: any) {
|
||||
if (
|
||||
new QMouseEvent(native).button() === 1 &&
|
||||
searchResults?.playlists
|
||||
) {
|
||||
history.push("/search/playlists", {
|
||||
searchQuery,
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
>{`<h2>Playlists</h2>`}</Text>
|
||||
<View style="flex: 1; justify-content: 'space-around'; align-items: 'center';">
|
||||
{placeholder}
|
||||
{searchResults?.playlists?.items.map((playlist, index) => (
|
||||
<PlaylistCard
|
||||
key={index + playlist.id}
|
||||
playlist={playlist}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export default Search;
|
||||
|
@ -5,34 +5,51 @@ import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
||||
import { GenreView } from "./PlaylistGenreView";
|
||||
|
||||
function SearchResultPlaylistCollection() {
|
||||
const location = useLocation<{ searchQuery: string }>();
|
||||
const { data: searchResults, fetchNextPage, hasNextPage, isFetchingNextPage, isError, isLoading, refetch } = useSpotifyInfiniteQuery<SpotifyApi.SearchResponse>(
|
||||
QueryCacheKeys.searchPlaylist,
|
||||
(spotifyApi, { pageParam }) => spotifyApi.searchPlaylists(location.state.searchQuery, { limit: 20, offset: pageParam }).then((res) => res.body),
|
||||
{
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.playlists?.next) {
|
||||
return (lastPage.playlists?.offset ?? 0) + (lastPage.playlists?.limit ?? 0);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return (
|
||||
<GenreView
|
||||
isError={isError}
|
||||
isLoading={isLoading || isFetchingNextPage}
|
||||
refetch={refetch}
|
||||
heading={"Search: " + location.state.searchQuery}
|
||||
playlists={
|
||||
(searchResults?.pages
|
||||
?.map((page) => page.playlists?.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.PlaylistObjectSimplified[]) ?? []
|
||||
}
|
||||
loadMore={hasNextPage ? () => fetchNextPage() : undefined}
|
||||
isLoadable={!isFetchingNextPage}
|
||||
/>
|
||||
);
|
||||
const location = useLocation<{ searchQuery: string }>();
|
||||
const {
|
||||
data: searchResults,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isError,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useSpotifyInfiniteQuery<SpotifyApi.SearchResponse>(
|
||||
QueryCacheKeys.searchPlaylist,
|
||||
(spotifyApi, { pageParam }) =>
|
||||
spotifyApi
|
||||
.searchPlaylists(location.state.searchQuery, {
|
||||
limit: 20,
|
||||
offset: pageParam,
|
||||
})
|
||||
.then((res) => res.body),
|
||||
{
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.playlists?.next) {
|
||||
return (
|
||||
(lastPage.playlists?.offset ?? 0) +
|
||||
(lastPage.playlists?.limit ?? 0)
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
return (
|
||||
<GenreView
|
||||
isError={isError}
|
||||
isLoading={isLoading || isFetchingNextPage}
|
||||
refetch={refetch}
|
||||
heading={"Search: " + location.state.searchQuery}
|
||||
playlists={
|
||||
(searchResults?.pages
|
||||
?.map((page) => page.playlists?.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.PlaylistObjectSimplified[]) ?? []
|
||||
}
|
||||
loadMore={hasNextPage ? () => fetchNextPage() : undefined}
|
||||
isLoadable={!isFetchingNextPage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchResultPlaylistCollection;
|
||||
|
@ -8,51 +8,79 @@ import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||
import { TrackButton } from "./shared/TrackButton";
|
||||
|
||||
function SearchResultSongsCollection() {
|
||||
const location = useLocation<{ searchQuery: string }>();
|
||||
const { data: searchResults, fetchNextPage, hasNextPage, isFetchingNextPage, isError, isLoading, refetch } = useSpotifyInfiniteQuery<SpotifyApi.SearchResponse>(
|
||||
QueryCacheKeys.searchSongs,
|
||||
(spotifyApi, { pageParam }) => spotifyApi.searchTracks(location.state.searchQuery, { limit: 20, offset: pageParam }).then((res) => res.body),
|
||||
{
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.tracks?.next) {
|
||||
return (lastPage.tracks?.offset ?? 0) + (lastPage.tracks?.limit ?? 0);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text>{`
|
||||
const location = useLocation<{ searchQuery: string }>();
|
||||
const {
|
||||
data: searchResults,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isError,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useSpotifyInfiniteQuery<SpotifyApi.SearchResponse>(
|
||||
QueryCacheKeys.searchSongs,
|
||||
(spotifyApi, { pageParam }) =>
|
||||
spotifyApi
|
||||
.searchTracks(location.state.searchQuery, {
|
||||
limit: 20,
|
||||
offset: pageParam,
|
||||
})
|
||||
.then((res) => res.body),
|
||||
{
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.tracks?.next) {
|
||||
return (lastPage.tracks?.offset ?? 0) + (lastPage.tracks?.limit ?? 0);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Text>{`
|
||||
<center>
|
||||
<h2>Search: ${location.state.searchQuery}</h2>
|
||||
</center>
|
||||
`}</Text>
|
||||
<TrackTableIndex />
|
||||
<ScrollArea style="flex: 1; border: none;">
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<PlaceholderApplet error={isError} loading={isLoading || isFetchingNextPage} message="Failed querying spotify" reload={refetch} />
|
||||
{searchResults?.pages
|
||||
.map((searchResult) => searchResult.tracks?.items)
|
||||
.filter(Boolean)
|
||||
.flat(1)
|
||||
.map((track, index) => track && <TrackButton key={index + track.id} index={index} track={track} />)}
|
||||
<TrackTableIndex />
|
||||
<ScrollArea style="flex: 1; border: none;">
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<PlaceholderApplet
|
||||
error={isError}
|
||||
loading={isLoading || isFetchingNextPage}
|
||||
message="Failed querying spotify"
|
||||
reload={refetch}
|
||||
/>
|
||||
{searchResults?.pages
|
||||
.map((searchResult) => searchResult.tracks?.items)
|
||||
.filter(Boolean)
|
||||
.flat(1)
|
||||
.map(
|
||||
(track, index) =>
|
||||
track && (
|
||||
<TrackButton
|
||||
key={index + track.id}
|
||||
index={index}
|
||||
track={track}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
style="flex-grow: 0; align-self: 'center';"
|
||||
text="Load more"
|
||||
on={{
|
||||
clicked() {
|
||||
fetchNextPage();
|
||||
},
|
||||
}}
|
||||
enabled={!isFetchingNextPage}
|
||||
/>
|
||||
)}
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
style="flex-grow: 0; align-self: 'center';"
|
||||
text="Load more"
|
||||
on={{
|
||||
clicked() {
|
||||
fetchNextPage();
|
||||
},
|
||||
}}
|
||||
enabled={!isFetchingNextPage}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
</ScrollArea>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchResultSongsCollection;
|
||||
|
@ -4,41 +4,48 @@ 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
|
||||
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>
|
||||
);
|
||||
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
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
|
||||
interface SettingsCheckTileProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
checked: boolean;
|
||||
onChange?: SwitchProps["onChange"];
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
checked: boolean;
|
||||
onChange?: SwitchProps["onChange"];
|
||||
}
|
||||
|
||||
export function SettingsCheckTile({ title, subtitle = "", onChange, checked }: SettingsCheckTileProps) {
|
||||
return (
|
||||
<View style="flex: 1; align-items: 'center'; padding: 15px 0; justify-content: 'space-between';">
|
||||
<Text>
|
||||
{`
|
||||
export function SettingsCheckTile({
|
||||
title,
|
||||
subtitle = "",
|
||||
onChange,
|
||||
checked,
|
||||
}: SettingsCheckTileProps) {
|
||||
return (
|
||||
<View style="flex: 1; align-items: 'center'; padding: 15px 0; justify-content: 'space-between';">
|
||||
<Text>
|
||||
{`
|
||||
<b>${title}</b>
|
||||
<p>${subtitle}</p>
|
||||
`}
|
||||
</Text>
|
||||
<Switch checked={checked} onChange={onChange} />
|
||||
</View>
|
||||
);
|
||||
</Text>
|
||||
<Switch checked={checked} onChange={onChange} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -6,27 +6,27 @@ import IconButton from "./shared/IconButton";
|
||||
import { QIcon } from "@nodegui/nodegui";
|
||||
|
||||
function TabMenu() {
|
||||
const history = useHistory();
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<View id="tabmenu" styleSheet={tabBarStylesheet}>
|
||||
<View>
|
||||
<Text>{`<h1>Spotube</h1>`}</Text>
|
||||
</View>
|
||||
<TabMenuItem url="/home" title="Browse" />
|
||||
<TabMenuItem url="/library" title="Library" />
|
||||
<TabMenuItem url="/currently" title="Currently Playing" />
|
||||
<TabMenuItem url="/search" title="Search" />
|
||||
<IconButton
|
||||
icon={new QIcon(settingsCog)}
|
||||
on={{
|
||||
clicked() {
|
||||
history.push("/settings");
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View id="tabmenu" styleSheet={tabBarStylesheet}>
|
||||
<View>
|
||||
<Text>{`<h1>Spotube</h1>`}</Text>
|
||||
</View>
|
||||
<TabMenuItem url="/home" title="Browse" />
|
||||
<TabMenuItem url="/library" title="Library" />
|
||||
<TabMenuItem url="/currently" title="Currently Playing" />
|
||||
<TabMenuItem url="/search" title="Search" />
|
||||
<IconButton
|
||||
icon={new QIcon(settingsCog)}
|
||||
on={{
|
||||
clicked() {
|
||||
history.push("/settings");
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export const tabBarStylesheet = `
|
||||
@ -47,21 +47,31 @@ export const tabBarStylesheet = `
|
||||
export default TabMenu;
|
||||
|
||||
export interface TabMenuItemProps {
|
||||
title: string;
|
||||
url: string;
|
||||
/**
|
||||
* path to the icon in string
|
||||
*/
|
||||
icon?: string;
|
||||
title: string;
|
||||
url: string;
|
||||
/**
|
||||
* path to the icon in string
|
||||
*/
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export function TabMenuItem({ icon, title, url }: TabMenuItemProps) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
export function TabMenuItem({ title, url }: TabMenuItemProps) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
function clicked() {
|
||||
history.push(url);
|
||||
}
|
||||
function clicked() {
|
||||
history.push(url);
|
||||
}
|
||||
|
||||
return <Button on={{ clicked }} id={location.pathname.replace("/", " ").startsWith(url.replace("/", " ")) ? "tabmenu-active-item" : `tabmenu-item`} text={title} />;
|
||||
return (
|
||||
<Button
|
||||
on={{ clicked }}
|
||||
id={
|
||||
location.pathname.replace("/", " ").startsWith(url.replace("/", " "))
|
||||
? "tabmenu-active-item"
|
||||
: `tabmenu-item`
|
||||
}
|
||||
text={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -6,48 +6,55 @@ import { getCachedImageBuffer } from "../../helpers/getCachedImageBuffer";
|
||||
import showError from "../../helpers/showError";
|
||||
|
||||
interface CachedImageProps extends Omit<ImageProps, "buffer"> {
|
||||
src: string;
|
||||
alt?: string;
|
||||
src: string;
|
||||
alt?: string;
|
||||
}
|
||||
|
||||
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();
|
||||
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, maxSize ?? size)
|
||||
.then((buffer) => setImageBuffer(buffer))
|
||||
.catch((error) => {
|
||||
setImageProcessError(false);
|
||||
showError(error, "[Cached Image Error]: ");
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
if (imageBuffer === undefined) {
|
||||
getCachedImageBuffer(src, maxSize ?? size)
|
||||
.then((buffer) => setImageBuffer(buffer))
|
||||
.catch((error) => {
|
||||
setImageProcessError(false);
|
||||
showError(error, "[Cached Image Error]: ");
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
labelRef.current?.close();
|
||||
};
|
||||
}, []);
|
||||
return () => {
|
||||
labelRef.current?.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (imageBuffer) {
|
||||
pixmap.loadFromData(imageBuffer);
|
||||
pixmap.scaled((size ?? maxSize)?.height ?? 100, (size ?? maxSize)?.width ?? 100);
|
||||
labelRef.current?.setPixmap(pixmap);
|
||||
}
|
||||
}, [imageBuffer]);
|
||||
useEffect(() => {
|
||||
if (imageBuffer) {
|
||||
pixmap.loadFromData(imageBuffer);
|
||||
pixmap.scaled(
|
||||
(size ?? maxSize)?.height ?? 100,
|
||||
(size ?? maxSize)?.width ?? 100,
|
||||
);
|
||||
labelRef.current?.setPixmap(pixmap);
|
||||
}
|
||||
}, [imageBuffer]);
|
||||
|
||||
return !imageProcessError && imageBuffer ? (
|
||||
<Text ref={labelRef} {...props}/>
|
||||
) : alt ? (
|
||||
<View style={`padding: ${((maxSize ?? size)?.height || 10) / 2.5}px ${((maxSize ?? size)?.width || 10) / 2.5}px;`}>
|
||||
<Text>{alt}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
return !imageProcessError && imageBuffer ? (
|
||||
<Text ref={labelRef} {...props} />
|
||||
) : alt ? (
|
||||
<View
|
||||
style={`padding: ${((maxSize ?? size)?.height || 10) / 2.5}px ${
|
||||
((maxSize ?? size)?.width || 10) / 2.5
|
||||
}px;`}
|
||||
>
|
||||
<Text>{alt}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
export default CachedImage;
|
||||
|
@ -5,10 +5,10 @@ import { useHistory } from "react-router";
|
||||
import PlaylistCard from "./PlaylistCard";
|
||||
|
||||
interface CategoryCardProps {
|
||||
url: string;
|
||||
name: string;
|
||||
isError: boolean;
|
||||
playlists: SpotifyApi.PlaylistObjectSimplified[];
|
||||
url: string;
|
||||
name: string;
|
||||
isError: boolean;
|
||||
playlists: SpotifyApi.PlaylistObjectSimplified[];
|
||||
}
|
||||
|
||||
const categoryStylesheet = `
|
||||
@ -36,26 +36,32 @@ const categoryStylesheet = `
|
||||
}
|
||||
`;
|
||||
const CategoryCard: FC<CategoryCardProps> = ({ name, isError, playlists, url }) => {
|
||||
const history = useHistory();
|
||||
function goToGenre(native: any) {
|
||||
const mouse = new QMouseEvent(native);
|
||||
if (mouse.button() === 1) {
|
||||
history.push(url, { name });
|
||||
const history = useHistory();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function goToGenre(native: any) {
|
||||
const mouse = new QMouseEvent(native);
|
||||
if (mouse.button() === 1) {
|
||||
history.push(url, { name });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isError) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<View id="container" styleSheet={categoryStylesheet}>
|
||||
<Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />
|
||||
<View id="child-view">
|
||||
{playlists.map((playlist, index) => {
|
||||
return <PlaylistCard key={index + playlist.id} playlist={playlist} />;
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
if (isError) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<View id="container" styleSheet={categoryStylesheet}>
|
||||
<Button
|
||||
id="anchor-heading"
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
on={{ MouseButtonRelease: goToGenre }}
|
||||
text={name}
|
||||
/>
|
||||
<View id="child-view">
|
||||
{playlists.map((playlist, index) => {
|
||||
return <PlaylistCard key={index + playlist.id} playlist={playlist} />;
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryCard;
|
||||
|
@ -1,20 +1,20 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import {QGraphicsDropShadowEffect, QPushButton} from "@nodegui/nodegui"
|
||||
import { QGraphicsDropShadowEffect, QPushButton } from "@nodegui/nodegui";
|
||||
import { Button } from "@nodegui/react-nodegui";
|
||||
import { ButtonProps } from "@nodegui/react-nodegui/dist/components/Button/RNButton";
|
||||
|
||||
interface IconButtonProps extends Omit<ButtonProps, "text"> {}
|
||||
type IconButtonProps = Omit<ButtonProps, "text">;
|
||||
|
||||
function IconButton({ style, ...props }: IconButtonProps) {
|
||||
const iconBtnRef = useRef<QPushButton>()
|
||||
const shadowGfx = new QGraphicsDropShadowEffect();
|
||||
useEffect(() => {
|
||||
shadowGfx.setBlurRadius(5);
|
||||
shadowGfx.setXOffset(0);
|
||||
shadowGfx.setYOffset(0);
|
||||
iconBtnRef.current?.setGraphicsEffect(shadowGfx);
|
||||
}, [])
|
||||
const iconButtonStyleSheet = `
|
||||
const iconBtnRef = useRef<QPushButton>();
|
||||
const shadowGfx = new QGraphicsDropShadowEffect();
|
||||
useEffect(() => {
|
||||
shadowGfx.setBlurRadius(5);
|
||||
shadowGfx.setXOffset(0);
|
||||
shadowGfx.setYOffset(0);
|
||||
iconBtnRef.current?.setGraphicsEffect(shadowGfx);
|
||||
}, []);
|
||||
const iconButtonStyleSheet = `
|
||||
#icon-btn{
|
||||
background-color: rgba(255, 255, 255, 0.055);
|
||||
border-width: 1px;
|
||||
@ -32,7 +32,15 @@ function IconButton({ style, ...props }: IconButtonProps) {
|
||||
}
|
||||
`;
|
||||
|
||||
return <Button ref={iconBtnRef} id="icon-btn" size={{ height: 30, width: 30, fixed: true }} styleSheet={iconButtonStyleSheet} {...props} />;
|
||||
return (
|
||||
<Button
|
||||
ref={iconBtnRef}
|
||||
id="icon-btn"
|
||||
size={{ height: 30, width: 30, fixed: true }}
|
||||
styleSheet={iconButtonStyleSheet}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default IconButton;
|
||||
|
@ -1,56 +1,56 @@
|
||||
import { View, Button, Text } from "@nodegui/react-nodegui";
|
||||
import { QLabel, QMovie, } from "@nodegui/nodegui";
|
||||
import { QLabel, QMovie } from "@nodegui/nodegui";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { loadingSpinner } from "../../icons";
|
||||
|
||||
interface ErrorAppletProps {
|
||||
error: boolean;
|
||||
loading: boolean;
|
||||
message?: string;
|
||||
reload: Function;
|
||||
helps?: boolean;
|
||||
error: boolean;
|
||||
loading: boolean;
|
||||
message?: string;
|
||||
reload: () => void;
|
||||
helps?: boolean;
|
||||
}
|
||||
|
||||
function PlaceholderApplet({ message, reload, helps, loading, error }: ErrorAppletProps) {
|
||||
const textRef = useRef<QLabel>();
|
||||
const movie = new QMovie();
|
||||
const textRef = useRef<QLabel>();
|
||||
const movie = new QMovie();
|
||||
|
||||
useEffect(() => {
|
||||
movie.setFileName(loadingSpinner);
|
||||
textRef.current?.setMovie(movie);
|
||||
textRef.current?.show();
|
||||
movie.start();
|
||||
}, []);
|
||||
if (loading) {
|
||||
return (
|
||||
<View style="flex: 1; justify-content: 'center'; align-items: 'center';">
|
||||
<Text ref={textRef} />
|
||||
</View>
|
||||
);
|
||||
} else if (error) {
|
||||
return (
|
||||
<View style="flex-direction: 'column'; align-items: 'center';">
|
||||
<Text openExternalLinks>{`
|
||||
useEffect(() => {
|
||||
movie.setFileName(loadingSpinner);
|
||||
textRef.current?.setMovie(movie);
|
||||
textRef.current?.show();
|
||||
movie.start();
|
||||
}, []);
|
||||
if (loading) {
|
||||
return (
|
||||
<View style="flex: 1; justify-content: 'center'; align-items: 'center';">
|
||||
<Text ref={textRef} />
|
||||
</View>
|
||||
);
|
||||
} else if (error) {
|
||||
return (
|
||||
<View style="flex-direction: 'column'; align-items: 'center';">
|
||||
<Text openExternalLinks>{`
|
||||
<h3>${message ? message : "An error occured"}</h3>
|
||||
${
|
||||
helps
|
||||
? `<p>Check if you're connected to internet & then try again. If the issue still persists ask for help or create a <a href="https://github.com/krtirtho/spotube/issues">issue</a>
|
||||
helps
|
||||
? `<p>Check if you're connected to internet & then try again. If the issue still persists ask for help or create a <a href="https://github.com/krtirtho/spotube/issues">issue</a>
|
||||
</p>`
|
||||
: ``
|
||||
: ``
|
||||
}
|
||||
`}</Text>
|
||||
<Button
|
||||
on={{
|
||||
clicked() {
|
||||
reload();
|
||||
},
|
||||
}}
|
||||
text="Reload"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
<Button
|
||||
on={{
|
||||
clicked() {
|
||||
reload();
|
||||
},
|
||||
}}
|
||||
text="Reload"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default PlaceholderApplet;
|
||||
|
@ -15,49 +15,56 @@ import CachedImage from "./CachedImage";
|
||||
import IconButton from "./IconButton";
|
||||
|
||||
interface PlaylistCardProps {
|
||||
playlist: SpotifyApi.PlaylistObjectSimplified;
|
||||
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);
|
||||
const { setCurrentTrack, currentPlaylist, setCurrentPlaylist } = useContext(playerContext);
|
||||
const { refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>([QueryCacheKeys.playlistTracks, id], (spotifyApi) => spotifyApi.getPlaylistTracks(id).then((track) => track.body.items), {
|
||||
initialData: [],
|
||||
enabled: false,
|
||||
});
|
||||
const { reactToPlaylist, isFavorite } = usePlaylistReaction();
|
||||
const preferences = useContext(preferencesContext);
|
||||
const thumbnail = playlist.images[0].url;
|
||||
const { id, description, name } = playlist;
|
||||
const history = useHistory();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const { setCurrentTrack, currentPlaylist, setCurrentPlaylist } =
|
||||
useContext(playerContext);
|
||||
const { refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>(
|
||||
[QueryCacheKeys.playlistTracks, id],
|
||||
(spotifyApi) =>
|
||||
spotifyApi.getPlaylistTracks(id).then((track) => track.body.items),
|
||||
{
|
||||
initialData: [],
|
||||
enabled: false,
|
||||
},
|
||||
);
|
||||
const { reactToPlaylist, isFavorite } = usePlaylistReaction();
|
||||
|
||||
const handlePlaylistPlayPause = async () => {
|
||||
try {
|
||||
const { data: tracks, isSuccess } = await refetch();
|
||||
if (currentPlaylist?.id !== id && isSuccess && tracks) {
|
||||
setCurrentPlaylist({ tracks, id, name, thumbnail });
|
||||
setCurrentTrack(tracks[0].track);
|
||||
} else {
|
||||
await audioPlayer.stop();
|
||||
setCurrentTrack(undefined);
|
||||
setCurrentPlaylist(undefined);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, "[Failed adding playlist to queue]: ");
|
||||
const handlePlaylistPlayPause = async () => {
|
||||
try {
|
||||
const { data: tracks, isSuccess } = await refetch();
|
||||
if (currentPlaylist?.id !== id && isSuccess && tracks) {
|
||||
setCurrentPlaylist({ tracks, id, name, thumbnail });
|
||||
setCurrentTrack(tracks[0].track);
|
||||
} else {
|
||||
await audioPlayer.stop();
|
||||
setCurrentTrack(undefined);
|
||||
setCurrentPlaylist(undefined);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error, "[Failed adding playlist to queue]: ");
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function gotoPlaylist(native?: any) {
|
||||
const key = new QMouseEvent(native);
|
||||
if (key.button() === 1) {
|
||||
history.push(`/playlist/${id}`, { name, thumbnail });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function gotoPlaylist(native?: any) {
|
||||
const key = new QMouseEvent(native);
|
||||
if (key.button() === 1) {
|
||||
history.push(`/playlist/${id}`, { name, thumbnail });
|
||||
}
|
||||
}
|
||||
const bgColor1 = useMemo(() => generateRandomColor(), []);
|
||||
const color = useMemo(() => getDarkenForeground(bgColor1), [bgColor1]);
|
||||
|
||||
const bgColor1 = useMemo(() => generateRandomColor(), []);
|
||||
const color = useMemo(() => getDarkenForeground(bgColor1), [bgColor1]);
|
||||
|
||||
const playlistStyleSheet = `
|
||||
const playlistStyleSheet = `
|
||||
#playlist-container, #img-container{
|
||||
width: 150px;
|
||||
padding: 10px;
|
||||
@ -76,70 +83,85 @@ const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
|
||||
border: 5px solid green;
|
||||
}
|
||||
`;
|
||||
const playlistAction = `
|
||||
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);
|
||||
},
|
||||
};
|
||||
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={preferences.playlistImages ? "img-container" : "playlist-container"}
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
styleSheet={playlistStyleSheet}
|
||||
on={{
|
||||
MouseButtonRelease: gotoPlaylist,
|
||||
...hovers,
|
||||
}}>
|
||||
{preferences.playlistImages && <CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} />}
|
||||
return (
|
||||
<View
|
||||
id={preferences.playlistImages ? "img-container" : "playlist-container"}
|
||||
cursor={CursorShape.PointingHandCursor}
|
||||
styleSheet={playlistStyleSheet}
|
||||
on={{
|
||||
MouseButtonRelease: gotoPlaylist,
|
||||
...hovers,
|
||||
}}
|
||||
>
|
||||
{preferences.playlistImages && (
|
||||
<CachedImage
|
||||
src={thumbnail}
|
||||
maxSize={{ height: 150, width: 150 }}
|
||||
scaledContents
|
||||
alt={name}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Text style={`color: ${color};`} wordWrap on={{ MouseButtonRelease: gotoPlaylist, ...hovers }}>
|
||||
{`
|
||||
<Text
|
||||
style={`color: ${color};`}
|
||||
wordWrap
|
||||
on={{ MouseButtonRelease: gotoPlaylist, ...hovers }}
|
||||
>
|
||||
{`
|
||||
<center>
|
||||
<h3>${name}</h3>
|
||||
<p>${description}</p>
|
||||
</center>
|
||||
`}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
{(hovered || currentPlaylist?.id === id) && !preferences.playlistImages && playlistActions}
|
||||
{preferences.playlistImages &&
|
||||
<View style="flex: 1; justify-content: 'space-around';">{playlistActions}
|
||||
{(hovered || currentPlaylist?.id === id) &&
|
||||
!preferences.playlistImages &&
|
||||
playlistActions}
|
||||
{preferences.playlistImages && (
|
||||
<View style="flex: 1; justify-content: 'space-around';">
|
||||
{playlistActions}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaylistCard;
|
||||
|
@ -4,41 +4,42 @@ import { CheckBoxProps } from "@nodegui/react-nodegui/dist/components/CheckBox/R
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export interface SwitchProps extends Omit<CheckBoxProps, "on" | "icon" | "text"> {
|
||||
onChange?(checked: boolean): void;
|
||||
onChange?(checked: boolean): void;
|
||||
}
|
||||
|
||||
function Switch({ checked: derivedChecked, onChange, ...props }: SwitchProps) {
|
||||
const [checked, setChecked] = useState<boolean>(false);
|
||||
const [checked, setChecked] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (derivedChecked) {
|
||||
setChecked(derivedChecked);
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (derivedChecked) {
|
||||
setChecked(derivedChecked);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Slider
|
||||
value={checked ? 1 : 0}
|
||||
hasTracking
|
||||
mouseTracking
|
||||
orientation={Orientation.Horizontal}
|
||||
maximum={1}
|
||||
minimum={0}
|
||||
maxSize={{ width: 30, height: 20 }}
|
||||
on={{
|
||||
valueChanged(value) {
|
||||
onChange && onChange(value===1);
|
||||
},
|
||||
MouseButtonRelease(native: any) {
|
||||
const mouse = new QMouseEvent(native);
|
||||
if (mouse.button() === 1) {
|
||||
setChecked(!checked);
|
||||
}
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Slider
|
||||
value={checked ? 1 : 0}
|
||||
hasTracking
|
||||
mouseTracking
|
||||
orientation={Orientation.Horizontal}
|
||||
maximum={1}
|
||||
minimum={0}
|
||||
maxSize={{ width: 30, height: 20 }}
|
||||
on={{
|
||||
valueChanged(value) {
|
||||
onChange && onChange(value === 1);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
MouseButtonRelease(native: any) {
|
||||
const mouse = new QMouseEvent(native);
|
||||
if (mouse.button() === 1) {
|
||||
setChecked(!checked);
|
||||
}
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Switch;
|
||||
|
@ -8,71 +8,83 @@ import { heart, heartRegular, pause, play } from "../../icons";
|
||||
import IconButton from "./IconButton";
|
||||
|
||||
export interface TrackButtonPlaylistObject extends SpotifyApi.PlaylistBaseObject {
|
||||
follower?: SpotifyApi.FollowersObject;
|
||||
tracks: SpotifyApi.PagingObject<SpotifyApi.SavedTrackObject | SpotifyApi.PlaylistTrackObject>;
|
||||
follower?: SpotifyApi.FollowersObject;
|
||||
tracks: SpotifyApi.PagingObject<
|
||||
SpotifyApi.SavedTrackObject | SpotifyApi.PlaylistTrackObject
|
||||
>;
|
||||
}
|
||||
|
||||
export interface TrackButtonProps {
|
||||
track: SpotifyApi.TrackObjectFull;
|
||||
playlist?: TrackButtonPlaylistObject;
|
||||
index: number;
|
||||
track: SpotifyApi.TrackObjectFull;
|
||||
playlist?: TrackButtonPlaylistObject;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const TrackButton: FC<TrackButtonProps> = ({ track, index, playlist }) => {
|
||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack, currentTrack } = useContext(playerContext);
|
||||
const handlePlaylistPlayPause = (index: number) => {
|
||||
if (playlist && currentPlaylist?.id !== playlist.id) {
|
||||
const globalPlaylistObj = { id: playlist.id, name: playlist.name, thumbnail: playlist.images[0].url, tracks: playlist.tracks.items };
|
||||
setCurrentPlaylist(globalPlaylistObj);
|
||||
setCurrentTrack(playlist.tracks.items[index].track);
|
||||
}
|
||||
};
|
||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack, currentTrack } =
|
||||
useContext(playerContext);
|
||||
const handlePlaylistPlayPause = (index: number) => {
|
||||
if (playlist && currentPlaylist?.id !== playlist.id) {
|
||||
const globalPlaylistObj = {
|
||||
id: playlist.id,
|
||||
name: playlist.name,
|
||||
thumbnail: playlist.images[0].url,
|
||||
tracks: playlist.tracks.items,
|
||||
};
|
||||
setCurrentPlaylist(globalPlaylistObj);
|
||||
setCurrentTrack(playlist.tracks.items[index].track);
|
||||
}
|
||||
};
|
||||
|
||||
const trackClickHandler = (track: SpotifyApi.TrackObjectFull) => {
|
||||
setCurrentTrack(track);
|
||||
};
|
||||
const trackClickHandler = (track: SpotifyApi.TrackObjectFull) => {
|
||||
setCurrentTrack(track);
|
||||
};
|
||||
|
||||
const duration = useMemo(() => msToMinAndSec(track.duration_ms), []);
|
||||
const active = (currentPlaylist?.id === playlist?.id && currentTrack?.id === track.id) || currentTrack?.id === track.id;
|
||||
return (
|
||||
<View
|
||||
id={active ? "active" : "track-button"}
|
||||
styleSheet={trackButtonStyle}
|
||||
on={{
|
||||
MouseButtonRelease(native: any) {
|
||||
if (new QMouseEvent(native).button() === 1 && playlist) {
|
||||
handlePlaylistPlayPause(index);
|
||||
}
|
||||
},
|
||||
}}>
|
||||
<Text style="padding: 5px;">{index + 1}</Text>
|
||||
<View style="flex-direction: 'column'; width: '35%';">
|
||||
<Text>{`<h3>${track.name}</h3>`}</Text>
|
||||
<Text>{track.artists.map((artist) => artist.name).join(", ")}</Text>
|
||||
</View>
|
||||
<Text style="width: '25%';">{track.album.name}</Text>
|
||||
<Text style="width: '15%';">{duration}</Text>
|
||||
<View style="width: '15%'; padding: 5px; justify-content: 'space-around';">
|
||||
<IconButton
|
||||
icon={new QIcon(isFavorite(track.id) ? heart : heartRegular)}
|
||||
on={{
|
||||
clicked() {
|
||||
reactToTrack({ track, added_at: "" });
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={new QIcon(active ? pause : play)}
|
||||
on={{
|
||||
clicked() {
|
||||
trackClickHandler(track);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
const duration = useMemo(() => msToMinAndSec(track.duration_ms), []);
|
||||
const active =
|
||||
(currentPlaylist?.id === playlist?.id && currentTrack?.id === track.id) ||
|
||||
currentTrack?.id === track.id;
|
||||
return (
|
||||
<View
|
||||
id={active ? "active" : "track-button"}
|
||||
styleSheet={trackButtonStyle}
|
||||
on={{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
MouseButtonRelease(native: any) {
|
||||
if (new QMouseEvent(native).button() === 1 && playlist) {
|
||||
handlePlaylistPlayPause(index);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Text style="padding: 5px;">{index + 1}</Text>
|
||||
<View style="flex-direction: 'column'; width: '35%';">
|
||||
<Text>{`<h3>${track.name}</h3>`}</Text>
|
||||
<Text>{track.artists.map((artist) => artist.name).join(", ")}</Text>
|
||||
</View>
|
||||
<Text style="width: '25%';">{track.album.name}</Text>
|
||||
<Text style="width: '15%';">{duration}</Text>
|
||||
<View style="width: '15%'; padding: 5px; justify-content: 'space-around';">
|
||||
<IconButton
|
||||
icon={new QIcon(isFavorite(track.id) ? heart : heartRegular)}
|
||||
on={{
|
||||
clicked() {
|
||||
reactToTrack({ track, added_at: "" });
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={new QIcon(active ? pause : play)}
|
||||
on={{
|
||||
clicked() {
|
||||
trackClickHandler(track);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const trackButtonStyle = `
|
||||
|
35
src/conf.ts
35
src/conf.ts
@ -2,7 +2,7 @@ import dotenv from "dotenv";
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
const env = dotenv.config({ path: join(process.cwd(), ".env") }).parsed as any;
|
||||
dotenv.config({ path: join(process.cwd(), ".env") }).parsed;
|
||||
export const clientId = "";
|
||||
export const trace = process.argv.find((arg) => arg === "--trace") ?? false;
|
||||
export const redirectURI = "http://localhost:4304/auth/spotify/callback";
|
||||
@ -10,23 +10,24 @@ export const confDir = join(homedir(), ".config", "spotube");
|
||||
export const cacheDir = join(homedir(), ".cache", "spotube");
|
||||
|
||||
export enum QueryCacheKeys {
|
||||
categories = "categories",
|
||||
categoryPlaylists = "categoryPlaylists",
|
||||
featuredPlaylists = "featuredPlaylists",
|
||||
genrePlaylists = "genrePlaylists",
|
||||
playlistTracks = "playlistTracks",
|
||||
userPlaylists = "user-palylists",
|
||||
userSavedTracks = "user-saved-tracks",
|
||||
search = "search",
|
||||
searchPlaylist = "searchPlaylist",
|
||||
searchSongs = "searchSongs",
|
||||
categories = "categories",
|
||||
categoryPlaylists = "categoryPlaylists",
|
||||
featuredPlaylists = "featuredPlaylists",
|
||||
genrePlaylists = "genrePlaylists",
|
||||
playlistTracks = "playlistTracks",
|
||||
userPlaylists = "user-palylists",
|
||||
userSavedTracks = "user-saved-tracks",
|
||||
search = "search",
|
||||
searchPlaylist = "searchPlaylist",
|
||||
searchSongs = "searchSongs",
|
||||
followedArtists = "followed-artists",
|
||||
}
|
||||
|
||||
export enum LocalStorageKeys {
|
||||
credentials = "credentials",
|
||||
refresh_token = "refresh_token",
|
||||
preferences = "user-preferences",
|
||||
volume = "volume",
|
||||
cachedPlaylist = "cached-playlist",
|
||||
cachedTrack = "cached-track"
|
||||
credentials = "credentials",
|
||||
refresh_token = "refresh_token",
|
||||
preferences = "user-preferences",
|
||||
volume = "volume",
|
||||
cachedPlaylist = "cached-playlist",
|
||||
cachedTrack = "cached-track",
|
||||
}
|
||||
|
@ -2,23 +2,29 @@ import React, { Dispatch, SetStateAction } from "react";
|
||||
import { Credentials } from "../app";
|
||||
|
||||
export interface AuthContext {
|
||||
isLoggedIn: boolean;
|
||||
setIsLoggedIn: Dispatch<SetStateAction<boolean>>;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
access_token: string;
|
||||
setCredentials: Dispatch<SetStateAction<Credentials>>
|
||||
setAccess_token: Dispatch<SetStateAction<string>>;
|
||||
isLoggedIn: boolean;
|
||||
setIsLoggedIn: Dispatch<SetStateAction<boolean>>;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
access_token: string;
|
||||
setCredentials: Dispatch<SetStateAction<Credentials>>;
|
||||
setAccess_token: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const authContext = React.createContext<AuthContext>({
|
||||
isLoggedIn: false,
|
||||
setIsLoggedIn() {},
|
||||
access_token: "",
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
setCredentials(){},
|
||||
setAccess_token() {},
|
||||
isLoggedIn: false,
|
||||
setIsLoggedIn() {
|
||||
return;
|
||||
},
|
||||
access_token: "",
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
setCredentials() {
|
||||
return;
|
||||
},
|
||||
setAccess_token() {
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
export default authContext;
|
||||
|
@ -2,15 +2,27 @@ import React, { Dispatch, SetStateAction } from "react";
|
||||
|
||||
export type CurrentTrack = SpotifyApi.TrackObjectFull;
|
||||
|
||||
export type CurrentPlaylist = { tracks: (SpotifyApi.PlaylistTrackObject | SpotifyApi.SavedTrackObject)[]; id: string; name: string; thumbnail: string };
|
||||
export type CurrentPlaylist = {
|
||||
tracks: (SpotifyApi.PlaylistTrackObject | SpotifyApi.SavedTrackObject)[];
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnail: string;
|
||||
};
|
||||
|
||||
export interface PlayerContext {
|
||||
currentPlaylist?: CurrentPlaylist;
|
||||
currentTrack?: CurrentTrack;
|
||||
setCurrentPlaylist: Dispatch<SetStateAction<CurrentPlaylist | undefined>>;
|
||||
setCurrentTrack: Dispatch<SetStateAction<CurrentTrack | undefined>>;
|
||||
currentPlaylist?: CurrentPlaylist;
|
||||
currentTrack?: CurrentTrack;
|
||||
setCurrentPlaylist: Dispatch<SetStateAction<CurrentPlaylist | undefined>>;
|
||||
setCurrentTrack: Dispatch<SetStateAction<CurrentTrack | undefined>>;
|
||||
}
|
||||
|
||||
const playerContext = React.createContext<PlayerContext>({ setCurrentPlaylist() {}, setCurrentTrack() {} });
|
||||
const playerContext = React.createContext<PlayerContext>({
|
||||
setCurrentPlaylist() {
|
||||
return;
|
||||
},
|
||||
setCurrentTrack() {
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
export default playerContext;
|
||||
|
@ -1,15 +1,17 @@
|
||||
import React, { Dispatch, SetStateAction } from "react";
|
||||
|
||||
export interface PreferencesContextProperties {
|
||||
playlistImages: boolean;
|
||||
playlistImages: boolean;
|
||||
}
|
||||
export interface PreferencesContext extends PreferencesContextProperties {
|
||||
setPreferences: Dispatch<SetStateAction<PreferencesContextProperties>>;
|
||||
setPreferences: Dispatch<SetStateAction<PreferencesContextProperties>>;
|
||||
}
|
||||
|
||||
const preferencesContext = React.createContext<PreferencesContext>({
|
||||
playlistImages: false,
|
||||
setPreferences() { }
|
||||
playlistImages: false,
|
||||
setPreferences() {
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
export default preferencesContext;
|
||||
|
@ -1,10 +1,18 @@
|
||||
import color from "color";
|
||||
|
||||
export function generateRandomColor(lightness: number=70): string {
|
||||
return "hsl(" + 360 * Math.random() + "," + (25 + 70 * Math.random()) + "%," + (lightness + 10 * Math.random()) + "%)";
|
||||
export function generateRandomColor(lightness = 70): string {
|
||||
return (
|
||||
"hsl(" +
|
||||
360 * Math.random() +
|
||||
"," +
|
||||
(25 + 70 * Math.random()) +
|
||||
"%," +
|
||||
(lightness + 10 * Math.random()) +
|
||||
"%)"
|
||||
);
|
||||
}
|
||||
|
||||
export function getDarkenForeground(hslcolor: string): string {
|
||||
const adjustedColor = color(hslcolor);
|
||||
return adjustedColor.darken(.5).hex();
|
||||
const adjustedColor = color(hslcolor);
|
||||
return adjustedColor.darken(0.5).hex();
|
||||
}
|
||||
|
@ -1,49 +1,80 @@
|
||||
import axios from "axios";
|
||||
import htmlToText from "html-to-text";
|
||||
import showError from "./showError";
|
||||
const delim1 = '</div></div></div></div><div class="hwc"><div class="BNeawe tAd8D AP7Wnd"><div><div class="BNeawe tAd8D AP7Wnd">';
|
||||
const delim2 = '</div></div></div></div></div><div><span class="hwc"><div class="BNeawe uEec3 AP7Wnd">';
|
||||
const delim1 =
|
||||
'</div></div></div></div><div class="hwc"><div class="BNeawe tAd8D AP7Wnd"><div><div class="BNeawe tAd8D AP7Wnd">';
|
||||
const delim2 =
|
||||
'</div></div></div></div></div><div><span class="hwc"><div class="BNeawe uEec3 AP7Wnd">';
|
||||
const url = "https://www.google.com/search?q=";
|
||||
|
||||
export default async function fetchLyrics(artists: string, title: string) {
|
||||
let lyrics;
|
||||
try {
|
||||
console.log("[lyric query]:", `${url}${encodeURIComponent(title + " " + artists)}+lyrics`);
|
||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}+lyrics`, { responseType: "text" })).data;
|
||||
[, lyrics] = lyrics.split(delim1);
|
||||
[lyrics] = lyrics.split(delim2);
|
||||
} catch (err) {
|
||||
showError(err, "[Lyric Query Error]: ");
|
||||
let lyrics;
|
||||
try {
|
||||
console.log("[lyric query]:", `${url}${encodeURIComponent(title + " " + artists)}+song+lyrics`);
|
||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}+song+lyrics`)).data;
|
||||
[, lyrics] = lyrics.split(delim1);
|
||||
[lyrics] = lyrics.split(delim2);
|
||||
} catch (err_1) {
|
||||
showError(err_1, "[Lyric Query Error]: ");
|
||||
try {
|
||||
console.log("[lyric query]:", `${url}${encodeURIComponent(title + " " + artists)}+song`);
|
||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}+song`)).data;
|
||||
console.log(
|
||||
"[lyric query]:",
|
||||
`${url}${encodeURIComponent(title + " " + artists)}+lyrics`,
|
||||
);
|
||||
lyrics = (
|
||||
await axios.get<string>(
|
||||
`${url}${encodeURIComponent(title + " " + artists)}+lyrics`,
|
||||
{ responseType: "text" },
|
||||
)
|
||||
).data;
|
||||
[, lyrics] = lyrics.split(delim1);
|
||||
[lyrics] = lyrics.split(delim2);
|
||||
} catch (err_2) {
|
||||
showError(err_2, "[Lyric Query Error]: ");
|
||||
} catch (err) {
|
||||
showError(err, "[Lyric Query Error]: ");
|
||||
try {
|
||||
console.log("[lyric query]:", `${url}${encodeURIComponent(title + " " + artists)}`);
|
||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}`)).data;
|
||||
[, lyrics] = lyrics.split(delim1);
|
||||
[lyrics] = lyrics.split(delim2);
|
||||
} catch (err_3) {
|
||||
showError(err_3, "[Lyric Query Error]: ");
|
||||
lyrics = "Not Found";
|
||||
console.log(
|
||||
"[lyric query]:",
|
||||
`${url}${encodeURIComponent(title + " " + artists)}+song+lyrics`,
|
||||
);
|
||||
lyrics = (
|
||||
await axios.get<string>(
|
||||
`${url}${encodeURIComponent(title + " " + artists)}+song+lyrics`,
|
||||
)
|
||||
).data;
|
||||
[, lyrics] = lyrics.split(delim1);
|
||||
[lyrics] = lyrics.split(delim2);
|
||||
} catch (err_1) {
|
||||
showError(err_1, "[Lyric Query Error]: ");
|
||||
try {
|
||||
console.log(
|
||||
"[lyric query]:",
|
||||
`${url}${encodeURIComponent(title + " " + artists)}+song`,
|
||||
);
|
||||
lyrics = (
|
||||
await axios.get<string>(
|
||||
`${url}${encodeURIComponent(title + " " + artists)}+song`,
|
||||
)
|
||||
).data;
|
||||
[, lyrics] = lyrics.split(delim1);
|
||||
[lyrics] = lyrics.split(delim2);
|
||||
} catch (err_2) {
|
||||
showError(err_2, "[Lyric Query Error]: ");
|
||||
try {
|
||||
console.log(
|
||||
"[lyric query]:",
|
||||
`${url}${encodeURIComponent(title + " " + artists)}`,
|
||||
);
|
||||
lyrics = (
|
||||
await axios.get<string>(
|
||||
`${url}${encodeURIComponent(title + " " + artists)}`,
|
||||
)
|
||||
).data;
|
||||
[, lyrics] = lyrics.split(delim1);
|
||||
[lyrics] = lyrics.split(delim2);
|
||||
} catch (err_3) {
|
||||
showError(err_3, "[Lyric Query Error]: ");
|
||||
lyrics = "Not Found";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const rets = lyrics.split("\n");
|
||||
let final = "";
|
||||
for (const ret of rets) {
|
||||
final = `${final}${htmlToText.htmlToText(ret)}\n`;
|
||||
}
|
||||
return final.trim();
|
||||
const rets = lyrics.split("\n");
|
||||
let final = "";
|
||||
for (const ret of rets) {
|
||||
final = `${final}${htmlToText.htmlToText(ret)}\n`;
|
||||
}
|
||||
return final.trim();
|
||||
}
|
||||
|
@ -9,59 +9,83 @@ import du from "du";
|
||||
import { cacheDir } from "../conf";
|
||||
|
||||
interface ImageDimensions {
|
||||
height: number;
|
||||
width: number;
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const fsm = fs.promises;
|
||||
|
||||
export async function getCachedImageBuffer(name: string, dims?: ImageDimensions): Promise<Buffer> {
|
||||
try {
|
||||
const MB_5 = 5000000; //5 Megabytes
|
||||
const cacheImgFolder = path.join(cacheDir, "images");
|
||||
// for clearing up the cache if it reaches out of the size
|
||||
const cacheName = `${isUrl(name) ? name.split("/").slice(-1)[0] : name}.cnim`;
|
||||
const cacheImgPath = path.join(cacheImgFolder, cacheName);
|
||||
// checking if the cached image already exists or not
|
||||
if (fs.existsSync(cacheImgPath)) {
|
||||
// automatically removing cache after a certain 50 MB oversize
|
||||
if ((await du(cacheImgFolder)) > MB_5) {
|
||||
fs.rmdirSync(cacheImgFolder, { recursive: true });
|
||||
}
|
||||
const cachedImg = await fsm.readFile(cacheImgPath);
|
||||
const cachedImgMeta = (await Jimp.read(cachedImg)).bitmap;
|
||||
export async function getCachedImageBuffer(
|
||||
name: string,
|
||||
dims?: ImageDimensions,
|
||||
): Promise<Buffer> {
|
||||
try {
|
||||
const MB_5 = 5000000; //5 Megabytes
|
||||
const cacheImgFolder = path.join(cacheDir, "images");
|
||||
// for clearing up the cache if it reaches out of the size
|
||||
const cacheName = `${isUrl(name) ? name.split("/").slice(-1)[0] : name}.cnim`;
|
||||
const cacheImgPath = path.join(cacheImgFolder, cacheName);
|
||||
// checking if the cached image already exists or not
|
||||
if (fs.existsSync(cacheImgPath)) {
|
||||
// automatically removing cache after a certain 50 MB oversize
|
||||
if ((await du(cacheImgFolder)) > MB_5) {
|
||||
fs.rmdirSync(cacheImgFolder, { recursive: true });
|
||||
}
|
||||
const cachedImg = await fsm.readFile(cacheImgPath);
|
||||
const cachedImgMeta = (await Jimp.read(cachedImg)).bitmap;
|
||||
|
||||
// if the dimensions are changed then the previously cached
|
||||
// images are removed and replaced with a new one
|
||||
if (dims && (cachedImgMeta.height !== dims.height || cachedImgMeta.width !== dims?.width)) {
|
||||
fs.unlinkSync(cacheImgPath);
|
||||
return await imageResizeAndWrite(cachedImg, { cacheFolder: cacheImgFolder, cacheName, dims });
|
||||
}
|
||||
return cachedImg;
|
||||
} else {
|
||||
// finding no cache image fetching it through axios
|
||||
const { data: imgData } = await axios.get<Stream>(name, { responseType: "stream" });
|
||||
// converting axios stream to buffer
|
||||
const resImgBuf = await streamToBuffer(imgData);
|
||||
// creating cache_dir
|
||||
await fsm.mkdir(cacheImgFolder, { recursive: true });
|
||||
if (dims) {
|
||||
return await imageResizeAndWrite(resImgBuf, { cacheFolder: cacheImgFolder, cacheName, dims });
|
||||
}
|
||||
await fsm.writeFile(path.join(cacheImgFolder, cacheName), resImgBuf);
|
||||
return resImgBuf;
|
||||
// if the dimensions are changed then the previously cached
|
||||
// images are removed and replaced with a new one
|
||||
if (
|
||||
dims &&
|
||||
(cachedImgMeta.height !== dims.height ||
|
||||
cachedImgMeta.width !== dims?.width)
|
||||
) {
|
||||
fs.unlinkSync(cacheImgPath);
|
||||
return await imageResizeAndWrite(cachedImg, {
|
||||
cacheFolder: cacheImgFolder,
|
||||
cacheName,
|
||||
dims,
|
||||
});
|
||||
}
|
||||
return cachedImg;
|
||||
} else {
|
||||
// finding no cache image fetching it through axios
|
||||
const { data: imgData } = await axios.get<Stream>(name, {
|
||||
responseType: "stream",
|
||||
});
|
||||
// converting axios stream to buffer
|
||||
const resImgBuf = await streamToBuffer(imgData);
|
||||
// creating cache_dir
|
||||
await fsm.mkdir(cacheImgFolder, { recursive: true });
|
||||
if (dims) {
|
||||
return await imageResizeAndWrite(resImgBuf, {
|
||||
cacheFolder: cacheImgFolder,
|
||||
cacheName,
|
||||
dims,
|
||||
});
|
||||
}
|
||||
await fsm.writeFile(path.join(cacheImgFolder, cacheName), resImgBuf);
|
||||
return resImgBuf;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Error in Image Cache]: ", error);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Error in Image Cache]: ", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function imageResizeAndWrite(img: Buffer, { cacheFolder, cacheName, dims }: { dims: ImageDimensions; cacheFolder: string; cacheName: string }): Promise<Buffer> {
|
||||
// caching the images by resizing if the max/fixed (Width/Height)
|
||||
// is available in the args
|
||||
const resizedImg = (await Jimp.read(img)).resize(dims.width, dims.height);
|
||||
const resizedImgBuffer = await resizedImg.getBufferAsync(resizedImg._originalMime);
|
||||
await fsm.writeFile(path.join(cacheFolder, cacheName), resizedImgBuffer);
|
||||
return resizedImgBuffer;
|
||||
async function imageResizeAndWrite(
|
||||
img: Buffer,
|
||||
{
|
||||
cacheFolder,
|
||||
cacheName,
|
||||
dims,
|
||||
}: { dims: ImageDimensions; cacheFolder: string; cacheName: string },
|
||||
): Promise<Buffer> {
|
||||
// caching the images by resizing if the max/fixed (Width/Height)
|
||||
// is available in the args
|
||||
const resizedImg = (await Jimp.read(img)).resize(dims.width, dims.height);
|
||||
const resizedImgBuffer = await resizedImg.getBufferAsync(resizedImg._originalMime);
|
||||
await fsm.writeFile(path.join(cacheFolder, cacheName), resizedImgBuffer);
|
||||
return resizedImgBuffer;
|
||||
}
|
||||
|
@ -10,42 +10,69 @@ import { CurrentTrack } from "../context/playerContext";
|
||||
* @param {(Array<string | number>)} matches
|
||||
* @return {*} {number}
|
||||
*/
|
||||
export function includePercentage(src: string | Array<string | number>, matches: Array<string | number>): number {
|
||||
let count = 0;
|
||||
matches.forEach((match) => {
|
||||
if (src.includes(match.toString())) count++;
|
||||
});
|
||||
return (count / matches.length) * 100;
|
||||
export function includePercentage(
|
||||
src: string | Array<string | number>,
|
||||
matches: Array<string | number>,
|
||||
): number {
|
||||
let count = 0;
|
||||
matches.forEach((match) => {
|
||||
if (src.includes(match.toString())) count++;
|
||||
});
|
||||
return (count / matches.length) * 100;
|
||||
}
|
||||
|
||||
export interface YoutubeTrack extends CurrentTrack {
|
||||
youtube_uri: string;
|
||||
youtube_uri: string;
|
||||
}
|
||||
|
||||
export async function getYoutubeTrack(track: SpotifyApi.TrackObjectFull): Promise<YoutubeTrack> {
|
||||
try {
|
||||
const artistsName = track.artists.map((ar) => ar.name);
|
||||
const queryString = `${artistsName[0]} - ${track.name}${artistsName.length > 1 ? ` feat. ${artistsName.slice(1).join(" ")}` : ``}`;
|
||||
console.log("Youtube Query String:", queryString);
|
||||
const result = await scrapYt.search(queryString, { limit: 7, type: "video" });
|
||||
const tracksWithRelevance = result
|
||||
.map((video) => {
|
||||
// percentage of matched track {name, artists} matched with
|
||||
// title of the youtube search results
|
||||
const matchPercentage = includePercentage(video.title, [track.name, ...artistsName]);
|
||||
// keeps only those tracks which are from the same artist channel
|
||||
const sameChannel = video.channel.name.includes(artistsName[0]) || artistsName[0].includes(video.channel.name);
|
||||
return { url: `http://www.youtube.com/watch?v=${video.id}`, matchPercentage, sameChannel, id: track.id };
|
||||
})
|
||||
.sort((a, b) => (a.matchPercentage > b.matchPercentage ? -1 : 1));
|
||||
const sameChannelTracks = tracksWithRelevance.filter((tr) => tr.sameChannel);
|
||||
export async function getYoutubeTrack(
|
||||
track: SpotifyApi.TrackObjectFull,
|
||||
): Promise<YoutubeTrack> {
|
||||
try {
|
||||
const artistsName = track.artists.map((ar) => ar.name);
|
||||
const queryString = `${artistsName[0]} - ${track.name}${
|
||||
artistsName.length > 1 ? ` feat. ${artistsName.slice(1).join(" ")}` : ``
|
||||
}`;
|
||||
console.log("Youtube Query String:", queryString);
|
||||
const result = await scrapYt.search(queryString, { limit: 7, type: "video" });
|
||||
const tracksWithRelevance = result
|
||||
.map((video) => {
|
||||
// percentage of matched track {name, artists} matched with
|
||||
// title of the youtube search results
|
||||
const matchPercentage = includePercentage(video.title, [
|
||||
track.name,
|
||||
...artistsName,
|
||||
]);
|
||||
// keeps only those tracks which are from the same artist channel
|
||||
const sameChannel =
|
||||
video.channel.name.includes(artistsName[0]) ||
|
||||
artistsName[0].includes(video.channel.name);
|
||||
return {
|
||||
url: `http://www.youtube.com/watch?v=${video.id}`,
|
||||
matchPercentage,
|
||||
sameChannel,
|
||||
id: track.id,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (a.matchPercentage > b.matchPercentage ? -1 : 1));
|
||||
const sameChannelTracks = tracksWithRelevance.filter((tr) => tr.sameChannel);
|
||||
|
||||
const rarestTrack = result.map((res) => ({ url: `http://www.youtube.com/watch?v=${res.id}`, id: res.id }));
|
||||
const rarestTrack = result.map((res) => ({
|
||||
url: `http://www.youtube.com/watch?v=${res.id}`,
|
||||
id: res.id,
|
||||
}));
|
||||
|
||||
const finalTrack = { ...track, youtube_uri: (sameChannelTracks.length > 0 ? sameChannelTracks : tracksWithRelevance.length > 0 ? tracksWithRelevance : rarestTrack)[0].url };
|
||||
return finalTrack;
|
||||
} catch (error) {
|
||||
console.error("Failed to resolve track's youtube url: ", error);
|
||||
throw error;
|
||||
}
|
||||
const finalTrack = {
|
||||
...track,
|
||||
youtube_uri: (sameChannelTracks.length > 0
|
||||
? sameChannelTracks
|
||||
: tracksWithRelevance.length > 0
|
||||
? tracksWithRelevance
|
||||
: rarestTrack)[0].url,
|
||||
};
|
||||
return finalTrack;
|
||||
} catch (error) {
|
||||
console.error("Failed to resolve track's youtube url: ", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
export function msToMinAndSec(ms: number) {
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds:number = parseInt(((ms % 60000) / 1000).toFixed(0));
|
||||
return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds: number = parseInt(((ms % 60000) / 1000).toFixed(0));
|
||||
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { trace } from "../conf";
|
||||
import chalk from "chalk";
|
||||
|
||||
function showError(error: any, message: any="[Error]: ") {
|
||||
console.error(chalk.red(message), trace ? error : error.message);
|
||||
function showError(error: Error | TypeError, message = "[Error]: ") {
|
||||
console.error(chalk.red(message), trace ? error : error.message);
|
||||
}
|
||||
|
||||
export default showError;
|
@ -1,4 +1,4 @@
|
||||
export function shuffleArray<T>(array:T[]):T[] {
|
||||
export function shuffleArray<T>(array: T[]): T[] {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const temp = array[i];
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { Stream } from "stream";
|
||||
|
||||
export function streamToBuffer(stream: Stream): Promise<Buffer> {
|
||||
let buffArr: any[] = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on("data", (data) => {
|
||||
buffArr.push(data);
|
||||
const buffArr: Uint8Array[] = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on("data", (data) => {
|
||||
buffArr.push(data);
|
||||
});
|
||||
stream.on("end", async () => {
|
||||
resolve(Buffer.concat(buffArr));
|
||||
});
|
||||
stream.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
stream.on("end", async () => {
|
||||
resolve(Buffer.concat(buffArr));
|
||||
});
|
||||
stream.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -8,52 +8,52 @@ import playerContext from "../context/playerContext";
|
||||
import showError from "../helpers/showError";
|
||||
|
||||
function useDownloadQueue() {
|
||||
const [downloadQueue, setDownloadQueue] = useState<YoutubeTrack[]>([]);
|
||||
const [completedQueue, setCompletedQueue] = useState<YoutubeTrack[]>([]);
|
||||
const { currentTrack } = useContext(playerContext);
|
||||
const [downloadQueue, setDownloadQueue] = useState<YoutubeTrack[]>([]);
|
||||
const [completedQueue, setCompletedQueue] = useState<YoutubeTrack[]>([]);
|
||||
const { currentTrack } = useContext(playerContext);
|
||||
|
||||
function addToQueue(obj: YoutubeTrack) {
|
||||
setDownloadQueue([...downloadQueue, obj]);
|
||||
}
|
||||
const completedTrackIds = completedQueue.map((x) => x.id);
|
||||
const downloadingTrackIds = downloadQueue.map((x) => x.id);
|
||||
function addToQueue(obj: YoutubeTrack) {
|
||||
setDownloadQueue([...downloadQueue, obj]);
|
||||
}
|
||||
const completedTrackIds = completedQueue.map((x) => x.id);
|
||||
const downloadingTrackIds = downloadQueue.map((x) => x.id);
|
||||
|
||||
function isActiveDownloading() {
|
||||
return downloadingTrackIds.includes(currentTrack?.id ?? "");
|
||||
}
|
||||
function isActiveDownloading() {
|
||||
return downloadingTrackIds.includes(currentTrack?.id ?? "");
|
||||
}
|
||||
|
||||
function isFinishedDownloading() {
|
||||
return completedTrackIds.includes(currentTrack?.id ?? "");
|
||||
}
|
||||
function isFinishedDownloading() {
|
||||
return completedTrackIds.includes(currentTrack?.id ?? "");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
downloadQueue.forEach(async (el) => {
|
||||
if (!completedTrackIds.includes(el.id)) {
|
||||
ytdl(el.youtube_uri, {
|
||||
filter: "audioonly",
|
||||
})
|
||||
.pipe(
|
||||
fs.createWriteStream(
|
||||
join(
|
||||
os.homedir(),
|
||||
"Music",
|
||||
`${el.name} - ${el.artists
|
||||
.map((x) => x.name)
|
||||
.join(", ")
|
||||
.trim()}.mp3`
|
||||
)
|
||||
)
|
||||
)
|
||||
.on("error", (err) => {
|
||||
showError(err, `[failed to download ${el.name}]: `);
|
||||
})
|
||||
.on("finish", () => {
|
||||
setCompletedQueue([...completedQueue, el]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [downloadQueue]);
|
||||
return { addToQueue, isFinishedDownloading, isActiveDownloading };
|
||||
useEffect(() => {
|
||||
downloadQueue.forEach(async (el) => {
|
||||
if (!completedTrackIds.includes(el.id)) {
|
||||
ytdl(el.youtube_uri, {
|
||||
filter: "audioonly",
|
||||
})
|
||||
.pipe(
|
||||
fs.createWriteStream(
|
||||
join(
|
||||
os.homedir(),
|
||||
"Music",
|
||||
`${el.name} - ${el.artists
|
||||
.map((x) => x.name)
|
||||
.join(", ")
|
||||
.trim()}.mp3`,
|
||||
),
|
||||
),
|
||||
)
|
||||
.on("error", (err) => {
|
||||
showError(err, `[failed to download ${el.name}]: `);
|
||||
})
|
||||
.on("finish", () => {
|
||||
setCompletedQueue([...completedQueue, el]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [downloadQueue]);
|
||||
return { addToQueue, isFinishedDownloading, isActiveDownloading };
|
||||
}
|
||||
|
||||
export default useDownloadQueue;
|
||||
|
@ -4,51 +4,72 @@ import useSpotifyInfiniteQuery from "./useSpotifyInfiniteQuery";
|
||||
import useSpotifyMutation from "./useSpotifyMutation";
|
||||
|
||||
function usePlaylistReaction() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: favoritePagedPlaylists } = useSpotifyInfiniteQuery<SpotifyApi.ListOfUsersPlaylistsResponse>(QueryCacheKeys.userPlaylists, (spotifyApi, { pageParam }) =>
|
||||
spotifyApi.getUserPlaylists({ limit: 20, offset: pageParam }).then((userPlaylists) => {
|
||||
return userPlaylists.body;
|
||||
})
|
||||
);
|
||||
const favoritePlaylists = favoritePagedPlaylists?.pages
|
||||
.map((playlist) => playlist.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.PlaylistObjectSimplified[];
|
||||
const queryClient = useQueryClient();
|
||||
const { data: favoritePagedPlaylists } =
|
||||
useSpotifyInfiniteQuery<SpotifyApi.ListOfUsersPlaylistsResponse>(
|
||||
QueryCacheKeys.userPlaylists,
|
||||
(spotifyApi, { pageParam }) =>
|
||||
spotifyApi
|
||||
.getUserPlaylists({ limit: 20, offset: pageParam })
|
||||
.then((userPlaylists) => {
|
||||
return userPlaylists.body;
|
||||
}),
|
||||
);
|
||||
const favoritePlaylists = favoritePagedPlaylists?.pages
|
||||
.map((playlist) => playlist.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.PlaylistObjectSimplified[];
|
||||
|
||||
function updateFunction(playlist: SpotifyApi.PlaylistObjectSimplified, old?: InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse>): InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse> {
|
||||
const obj: typeof old = {
|
||||
pageParams: old?.pageParams ?? [],
|
||||
pages:
|
||||
old?.pages.map(
|
||||
(oldPage, index): SpotifyApi.ListOfUsersPlaylistsResponse => {
|
||||
const isPlaylistFavorite = isFavorite(playlist.id);
|
||||
if (index === 0 && !isPlaylistFavorite) {
|
||||
return { ...oldPage, items: [...oldPage.items, playlist] };
|
||||
} else if (isPlaylistFavorite) {
|
||||
return { ...oldPage, items: oldPage.items.filter((oldPlaylist) => oldPlaylist.id !== playlist.id) };
|
||||
}
|
||||
return oldPage;
|
||||
}
|
||||
) ?? [],
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
|
||||
const { mutate: reactToPlaylist } = useSpotifyMutation<{}, SpotifyApi.PlaylistObjectSimplified>(
|
||||
(spotifyApi, { id }) => spotifyApi[isFavorite(id) ? "unfollowPlaylist" : "followPlaylist"](id).then((res) => res.body),
|
||||
{
|
||||
onSuccess(_, playlist) {
|
||||
queryClient.setQueryData<InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse>>(QueryCacheKeys.userPlaylists, (old) => updateFunction(playlist, old));
|
||||
},
|
||||
function updateFunction(
|
||||
playlist: SpotifyApi.PlaylistObjectSimplified,
|
||||
old?: InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse>,
|
||||
): InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse> {
|
||||
const obj: typeof old = {
|
||||
pageParams: old?.pageParams ?? [],
|
||||
pages:
|
||||
old?.pages.map(
|
||||
(oldPage, index): SpotifyApi.ListOfUsersPlaylistsResponse => {
|
||||
const isPlaylistFavorite = isFavorite(playlist.id);
|
||||
if (index === 0 && !isPlaylistFavorite) {
|
||||
return { ...oldPage, items: [...oldPage.items, playlist] };
|
||||
} else if (isPlaylistFavorite) {
|
||||
return {
|
||||
...oldPage,
|
||||
items: oldPage.items.filter(
|
||||
(oldPlaylist) => oldPlaylist.id !== playlist.id,
|
||||
),
|
||||
};
|
||||
}
|
||||
return oldPage;
|
||||
},
|
||||
) ?? [],
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
);
|
||||
const favoritePlaylistIds = favoritePlaylists?.map((playlist) => playlist.id);
|
||||
|
||||
function isFavorite(playlistId: string) {
|
||||
return favoritePlaylistIds?.includes(playlistId);
|
||||
}
|
||||
const { mutate: reactToPlaylist } = useSpotifyMutation<
|
||||
unknown,
|
||||
SpotifyApi.PlaylistObjectSimplified
|
||||
>(
|
||||
(spotifyApi, { id }) =>
|
||||
spotifyApi[isFavorite(id) ? "unfollowPlaylist" : "followPlaylist"](id).then(
|
||||
(res) => res.body,
|
||||
),
|
||||
{
|
||||
onSuccess(_, playlist) {
|
||||
queryClient.setQueryData<
|
||||
InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse>
|
||||
>(QueryCacheKeys.userPlaylists, (old) => updateFunction(playlist, old));
|
||||
},
|
||||
},
|
||||
);
|
||||
const favoritePlaylistIds = favoritePlaylists?.map((playlist) => playlist.id);
|
||||
|
||||
return { reactToPlaylist, isFavorite, favoritePlaylists };
|
||||
function isFavorite(playlistId: string) {
|
||||
return favoritePlaylistIds?.includes(playlistId);
|
||||
}
|
||||
|
||||
return { reactToPlaylist, isFavorite, favoritePlaylists };
|
||||
}
|
||||
|
||||
export default usePlaylistReaction;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import chalk from "chalk";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { LocalStorageKeys } from "../conf";
|
||||
import authContext from "../context/authContext";
|
||||
@ -6,35 +5,30 @@ import showError from "../helpers/showError";
|
||||
import spotifyApi from "../initializations/spotifyApi";
|
||||
|
||||
function useSpotifyApi() {
|
||||
const {
|
||||
access_token,
|
||||
clientId,
|
||||
clientSecret,
|
||||
isLoggedIn,
|
||||
setAccess_token,
|
||||
} = useContext(authContext);
|
||||
const refreshToken = localStorage.getItem(LocalStorageKeys.refresh_token);
|
||||
const { access_token, clientId, clientSecret, isLoggedIn, setAccess_token } =
|
||||
useContext(authContext);
|
||||
const refreshToken = localStorage.getItem(LocalStorageKeys.refresh_token);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn && clientId && clientSecret && refreshToken) {
|
||||
spotifyApi.setClientId(clientId);
|
||||
spotifyApi.setClientSecret(clientSecret);
|
||||
spotifyApi.setRefreshToken(refreshToken);
|
||||
if (!access_token) {
|
||||
spotifyApi
|
||||
.refreshAccessToken()
|
||||
.then((token) => {
|
||||
setAccess_token(token.body.access_token);
|
||||
})
|
||||
.catch((error) => {
|
||||
showError(error);
|
||||
});
|
||||
}
|
||||
spotifyApi.setAccessToken(access_token);
|
||||
}
|
||||
}, [access_token, clientId, clientSecret, isLoggedIn]);
|
||||
useEffect(() => {
|
||||
if (isLoggedIn && clientId && clientSecret && refreshToken) {
|
||||
spotifyApi.setClientId(clientId);
|
||||
spotifyApi.setClientSecret(clientSecret);
|
||||
spotifyApi.setRefreshToken(refreshToken);
|
||||
if (!access_token) {
|
||||
spotifyApi
|
||||
.refreshAccessToken()
|
||||
.then((token) => {
|
||||
setAccess_token(token.body.access_token);
|
||||
})
|
||||
.catch((error) => {
|
||||
showError(error);
|
||||
});
|
||||
}
|
||||
spotifyApi.setAccessToken(access_token);
|
||||
}
|
||||
}, [access_token, clientId, clientSecret, isLoggedIn]);
|
||||
|
||||
return spotifyApi;
|
||||
return spotifyApi;
|
||||
}
|
||||
|
||||
export default useSpotifyApi;
|
||||
|
@ -5,25 +5,29 @@ import authContext from "../context/authContext";
|
||||
import showError from "../helpers/showError";
|
||||
|
||||
function useSpotifyApiError(spotifyApi: SpotifyWebApi) {
|
||||
const { setAccess_token, isLoggedIn } = useContext(authContext);
|
||||
return async (error: any | Error | TypeError) => {
|
||||
const isUnauthorized = error.message === "Unauthorized";
|
||||
const status401 = error.status === 401;
|
||||
const bodyStatus401 = error.body.error.status === 401;
|
||||
const noToken = error.body.error.message === "No token provided";
|
||||
const expiredToken = error.body.error.message === "The access token expired";
|
||||
if ((isUnauthorized && isLoggedIn && status401) || ((noToken || expiredToken) && bodyStatus401)) {
|
||||
try {
|
||||
console.log(chalk.bgYellow.blackBright("Refreshing Access token"));
|
||||
const {
|
||||
body: { access_token: refreshedAccessToken },
|
||||
} = await spotifyApi.refreshAccessToken();
|
||||
setAccess_token(refreshedAccessToken);
|
||||
} catch (error) {
|
||||
showError(error, "[Authorization Failure]: ");
|
||||
}
|
||||
}
|
||||
};
|
||||
const { setAccess_token, isLoggedIn } = useContext(authContext);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return async (error: SpotifyApi.ErrorObject | any) => {
|
||||
const isUnauthorized = error.message === "Unauthorized";
|
||||
const status401 = error.status === 401;
|
||||
const bodyStatus401 = error.body.error.status === 401;
|
||||
const noToken = error.body.error.message === "No token provided";
|
||||
const expiredToken = error.body.error.message === "The access token expired";
|
||||
if (
|
||||
(isUnauthorized && isLoggedIn && status401) ||
|
||||
((noToken || expiredToken) && bodyStatus401)
|
||||
) {
|
||||
try {
|
||||
console.log(chalk.bgYellow.blackBright("Refreshing Access token"));
|
||||
const {
|
||||
body: { access_token: refreshedAccessToken },
|
||||
} = await spotifyApi.refreshAccessToken();
|
||||
setAccess_token(refreshedAccessToken);
|
||||
} catch (error) {
|
||||
showError(error, "[Authorization Failure]: ");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default useSpotifyApiError;
|
||||
|
@ -1,28 +1,41 @@
|
||||
import { useEffect } from "react";
|
||||
import { QueryFunctionContext, QueryKey, useInfiniteQuery, UseInfiniteQueryOptions, UseInfiniteQueryResult } from "react-query";
|
||||
import {
|
||||
QueryFunctionContext,
|
||||
QueryKey,
|
||||
useInfiniteQuery,
|
||||
UseInfiniteQueryOptions,
|
||||
UseInfiniteQueryResult,
|
||||
} from "react-query";
|
||||
import SpotifyWebApi from "spotify-web-api-node";
|
||||
import useSpotifyApi from "./useSpotifyApi";
|
||||
import useSpotifyApiError from "./useSpotifyApiError";
|
||||
|
||||
type SpotifyQueryFn<TQueryData> = (spotifyApi: SpotifyWebApi, pageArgs: QueryFunctionContext) => Promise<TQueryData>;
|
||||
type SpotifyQueryFn<TQueryData> = (
|
||||
spotifyApi: SpotifyWebApi,
|
||||
pageArgs: QueryFunctionContext,
|
||||
) => Promise<TQueryData>;
|
||||
|
||||
function useSpotifyInfiniteQuery<TQueryData = unknown>(
|
||||
queryKey: QueryKey,
|
||||
queryHandler: SpotifyQueryFn<TQueryData>,
|
||||
options: UseInfiniteQueryOptions<TQueryData, SpotifyApi.ErrorObject> = {}
|
||||
queryKey: QueryKey,
|
||||
queryHandler: SpotifyQueryFn<TQueryData>,
|
||||
options: UseInfiniteQueryOptions<TQueryData, SpotifyApi.ErrorObject> = {},
|
||||
): UseInfiniteQueryResult<TQueryData, SpotifyApi.ErrorObject> {
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
const query = useInfiniteQuery<TQueryData, SpotifyApi.ErrorObject>(queryKey, (pageArgs) => queryHandler(spotifyApi, pageArgs), options);
|
||||
const { isError, error } = query;
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
const query = useInfiniteQuery<TQueryData, SpotifyApi.ErrorObject>(
|
||||
queryKey,
|
||||
(pageArgs) => queryHandler(spotifyApi, pageArgs),
|
||||
options,
|
||||
);
|
||||
const { isError, error } = query;
|
||||
|
||||
useEffect(() => {
|
||||
if (isError && error) {
|
||||
handleSpotifyError(error);
|
||||
}
|
||||
}, [isError, error]);
|
||||
useEffect(() => {
|
||||
if (isError && error) {
|
||||
handleSpotifyError(error);
|
||||
}
|
||||
}, [isError, error]);
|
||||
|
||||
return query;
|
||||
return query;
|
||||
}
|
||||
|
||||
export default useSpotifyInfiniteQuery;
|
||||
|
@ -4,21 +4,30 @@ import SpotifyWebApi from "spotify-web-api-node";
|
||||
import useSpotifyApi from "./useSpotifyApi";
|
||||
import useSpotifyApiError from "./useSpotifyApiError";
|
||||
|
||||
type SpotifyMutationFn<TData = unknown, TVariables = unknown> = (spotifyApi: SpotifyWebApi, variables: TVariables) => Promise<TData>;
|
||||
type SpotifyMutationFn<TData = unknown, TVariables = unknown> = (
|
||||
spotifyApi: SpotifyWebApi,
|
||||
variables: TVariables,
|
||||
) => Promise<TData>;
|
||||
|
||||
function useSpotifyMutation<TData = unknown, TVariable = unknown>(mutationFn: SpotifyMutationFn<TData, TVariable>, options?: UseMutationOptions<TData, SpotifyApi.ErrorObject, TVariable>) {
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
const mutation = useMutation<TData, SpotifyApi.ErrorObject, TVariable>((arg) => mutationFn(spotifyApi, arg), options);
|
||||
const { isError, error } = mutation;
|
||||
function useSpotifyMutation<TData = unknown, TVariable = unknown>(
|
||||
mutationFn: SpotifyMutationFn<TData, TVariable>,
|
||||
options?: UseMutationOptions<TData, SpotifyApi.ErrorObject, TVariable>,
|
||||
) {
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
const mutation = useMutation<TData, SpotifyApi.ErrorObject, TVariable>(
|
||||
(arg) => mutationFn(spotifyApi, arg),
|
||||
options,
|
||||
);
|
||||
const { isError, error } = mutation;
|
||||
|
||||
useEffect(() => {
|
||||
if (isError && error) {
|
||||
handleSpotifyError(error);
|
||||
}
|
||||
}, [isError, error]);
|
||||
useEffect(() => {
|
||||
if (isError && error) {
|
||||
handleSpotifyError(error);
|
||||
}
|
||||
}, [isError, error]);
|
||||
|
||||
return mutation;
|
||||
return mutation;
|
||||
}
|
||||
|
||||
export default useSpotifyMutation;
|
||||
|
@ -7,23 +7,26 @@ import useSpotifyApiError from "./useSpotifyApiError";
|
||||
type SpotifyQueryFn<TQueryData> = (spotifyApi: SpotifyWebApi) => Promise<TQueryData>;
|
||||
|
||||
function useSpotifyQuery<TQueryData = unknown>(
|
||||
queryKey: QueryKey,
|
||||
queryHandler: SpotifyQueryFn<TQueryData>,
|
||||
options: UseQueryOptions<TQueryData, SpotifyApi.ErrorObject> = {}
|
||||
queryKey: QueryKey,
|
||||
queryHandler: SpotifyQueryFn<TQueryData>,
|
||||
options: UseQueryOptions<TQueryData, SpotifyApi.ErrorObject> = {},
|
||||
): UseQueryResult<TQueryData, SpotifyApi.ErrorObject> {
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
const query = useQuery<TQueryData, SpotifyApi.ErrorObject>(queryKey, ()=>queryHandler(spotifyApi), options);
|
||||
const { isError, error } = query;
|
||||
const spotifyApi = useSpotifyApi();
|
||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||
const query = useQuery<TQueryData, SpotifyApi.ErrorObject>(
|
||||
queryKey,
|
||||
() => queryHandler(spotifyApi),
|
||||
options,
|
||||
);
|
||||
const { isError, error } = query;
|
||||
|
||||
useEffect(() => {
|
||||
if (isError && error) {
|
||||
handleSpotifyError(error);
|
||||
}
|
||||
}, [isError, error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError && error) {
|
||||
handleSpotifyError(error);
|
||||
}
|
||||
}, [isError, error]);
|
||||
|
||||
return query;
|
||||
return query;
|
||||
}
|
||||
|
||||
export default useSpotifyQuery;
|
||||
|
@ -4,49 +4,68 @@ import useSpotifyInfiniteQuery from "./useSpotifyInfiniteQuery";
|
||||
import useSpotifyMutation from "./useSpotifyMutation";
|
||||
|
||||
function useTrackReaction() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: userSavedTracks } = useSpotifyInfiniteQuery<SpotifyApi.UsersSavedTracksResponse>(QueryCacheKeys.userSavedTracks, (spotifyApi, { pageParam }) =>
|
||||
spotifyApi.getMySavedTracks({ limit: 50, offset: pageParam }).then((res) => res.body)
|
||||
);
|
||||
const favoriteTracks = userSavedTracks?.pages
|
||||
?.map((page) => page.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.SavedTrackObject[] | undefined;
|
||||
const queryClient = useQueryClient();
|
||||
const { data: userSavedTracks } =
|
||||
useSpotifyInfiniteQuery<SpotifyApi.UsersSavedTracksResponse>(
|
||||
QueryCacheKeys.userSavedTracks,
|
||||
(spotifyApi, { pageParam }) =>
|
||||
spotifyApi
|
||||
.getMySavedTracks({ limit: 50, offset: pageParam })
|
||||
.then((res) => res.body),
|
||||
);
|
||||
const favoriteTracks = userSavedTracks?.pages
|
||||
?.map((page) => page.items)
|
||||
.filter(Boolean)
|
||||
.flat(1) as SpotifyApi.SavedTrackObject[] | undefined;
|
||||
|
||||
function updateFunction(track: SpotifyApi.SavedTrackObject, old?: InfiniteData<SpotifyApi.UsersSavedTracksResponse>): InfiniteData<SpotifyApi.UsersSavedTracksResponse> {
|
||||
const obj: typeof old = {
|
||||
pageParams: old?.pageParams ?? [],
|
||||
pages:
|
||||
old?.pages.map(
|
||||
(oldPage, index): SpotifyApi.UsersSavedTracksResponse => {
|
||||
const isTrackFavorite = isFavorite(track.track.id);
|
||||
if (index === 0 && !isTrackFavorite) {
|
||||
return { ...oldPage, items: [...oldPage.items, track] };
|
||||
} else if (isTrackFavorite) {
|
||||
return { ...oldPage, items: oldPage.items.filter((oldTrack) => oldTrack.track.id !== track.track.id) };
|
||||
}
|
||||
return oldPage;
|
||||
}
|
||||
) ?? [],
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
|
||||
const { mutate: reactToTrack } = useSpotifyMutation<{}, SpotifyApi.SavedTrackObject>(
|
||||
(spotifyApi, { track }) => spotifyApi[isFavorite(track.id) ? "removeFromMySavedTracks" : "addToMySavedTracks"]([track.id]).then((res) => res.body),
|
||||
{
|
||||
onSuccess(_, track) {
|
||||
queryClient.setQueryData<InfiniteData<SpotifyApi.UsersSavedTracksResponse>>(QueryCacheKeys.userSavedTracks, (old) => updateFunction(track, old));
|
||||
},
|
||||
function updateFunction(
|
||||
track: SpotifyApi.SavedTrackObject,
|
||||
old?: InfiniteData<SpotifyApi.UsersSavedTracksResponse>,
|
||||
): InfiniteData<SpotifyApi.UsersSavedTracksResponse> {
|
||||
const obj: typeof old = {
|
||||
pageParams: old?.pageParams ?? [],
|
||||
pages:
|
||||
old?.pages.map((oldPage, index): SpotifyApi.UsersSavedTracksResponse => {
|
||||
const isTrackFavorite = isFavorite(track.track.id);
|
||||
if (index === 0 && !isTrackFavorite) {
|
||||
return { ...oldPage, items: [...oldPage.items, track] };
|
||||
} else if (isTrackFavorite) {
|
||||
return {
|
||||
...oldPage,
|
||||
items: oldPage.items.filter(
|
||||
(oldTrack) => oldTrack.track.id !== track.track.id,
|
||||
),
|
||||
};
|
||||
}
|
||||
return oldPage;
|
||||
}) ?? [],
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
);
|
||||
const favoriteTrackIds = favoriteTracks?.map((track) => track.track.id);
|
||||
|
||||
function isFavorite(trackId: string) {
|
||||
return favoriteTrackIds?.includes(trackId);
|
||||
}
|
||||
const { mutate: reactToTrack } = useSpotifyMutation<
|
||||
unknown,
|
||||
SpotifyApi.SavedTrackObject
|
||||
>(
|
||||
(spotifyApi, { track }) =>
|
||||
spotifyApi[
|
||||
isFavorite(track.id) ? "removeFromMySavedTracks" : "addToMySavedTracks"
|
||||
]([track.id]).then((res) => res.body),
|
||||
{
|
||||
onSuccess(_, track) {
|
||||
queryClient.setQueryData<
|
||||
InfiniteData<SpotifyApi.UsersSavedTracksResponse>
|
||||
>(QueryCacheKeys.userSavedTracks, (old) => updateFunction(track, old));
|
||||
},
|
||||
},
|
||||
);
|
||||
const favoriteTrackIds = favoriteTracks?.map((track) => track.track.id);
|
||||
|
||||
return { reactToTrack, isFavorite, favoriteTracks };
|
||||
function isFavorite(trackId: string) {
|
||||
return favoriteTrackIds?.includes(trackId);
|
||||
}
|
||||
|
||||
return { reactToTrack, isFavorite, favoriteTracks };
|
||||
}
|
||||
|
||||
export default useTrackReaction;
|
||||
|
24
src/icons.ts
24
src/icons.ts
@ -1,18 +1,18 @@
|
||||
import _play from "../assets/play-solid.svg";
|
||||
import _pause from "../assets/pause-solid.svg"
|
||||
import _angleLeft from "../assets/angle-left-solid.svg"
|
||||
import _backward from "../assets/backward-solid.svg"
|
||||
import _forward from "../assets/forward-solid.svg"
|
||||
import _heartRegular from "../assets/heart-regular.svg"
|
||||
import _heart from "../assets/heart-solid.svg"
|
||||
import _random from "../assets/random-solid.svg"
|
||||
import _stop from "../assets/stop-solid.svg"
|
||||
import _pause from "../assets/pause-solid.svg";
|
||||
import _angleLeft from "../assets/angle-left-solid.svg";
|
||||
import _backward from "../assets/backward-solid.svg";
|
||||
import _forward from "../assets/forward-solid.svg";
|
||||
import _heartRegular from "../assets/heart-regular.svg";
|
||||
import _heart from "../assets/heart-solid.svg";
|
||||
import _random from "../assets/random-solid.svg";
|
||||
import _stop from "../assets/stop-solid.svg";
|
||||
import _search from "../assets/search-solid.svg";
|
||||
import _loadingSpinner from "../assets/loading-spinner.gif";
|
||||
import _settingsCog from "../assets/setting-cog.svg"
|
||||
import _times from "../assets/times-solid.svg"
|
||||
import _musicNode from "../assets/music-solid.svg"
|
||||
import _download from "../assets/download-solid.svg"
|
||||
import _settingsCog from "../assets/setting-cog.svg";
|
||||
import _times from "../assets/times-solid.svg";
|
||||
import _musicNode from "../assets/music-solid.svg";
|
||||
import _download from "../assets/download-solid.svg";
|
||||
|
||||
export const play = _play;
|
||||
export const pause = _pause;
|
||||
|
@ -4,15 +4,15 @@ import App from "./app";
|
||||
|
||||
process.title = "Spotube";
|
||||
Renderer.render(<App />, {
|
||||
onInit(reconciler) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
require("@nodegui/devtools").connectReactDevtools(reconciler);
|
||||
}
|
||||
},
|
||||
onInit(reconciler) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
require("@nodegui/devtools").connectReactDevtools(reconciler);
|
||||
}
|
||||
},
|
||||
});
|
||||
// This is for hot reloading (this will be stripped off in production by webpack)
|
||||
if (module.hot) {
|
||||
module.hot.accept(["./app"], function () {
|
||||
Renderer.forceUpdate();
|
||||
});
|
||||
module.hot.accept(["./app"], function () {
|
||||
Renderer.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
@ -12,50 +12,54 @@ import Search from "./components/Search";
|
||||
import SearchResultPlaylistCollection from "./components/SearchResultPlaylistCollection";
|
||||
import SearchResultSongsCollection from "./components/SearchResultSongsCollection";
|
||||
import Settings from "./components/Settings";
|
||||
import Artist from "./components/Artist";
|
||||
|
||||
function Routes() {
|
||||
const { isLoggedIn } = useContext(authContext);
|
||||
return (
|
||||
<>
|
||||
<Route path="/">
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
<Redirect from="/" to="/home" />
|
||||
<TabMenu />
|
||||
<Route exact path="/home">
|
||||
<Home />
|
||||
const { isLoggedIn } = useContext(authContext);
|
||||
return (
|
||||
<>
|
||||
<Route path="/">
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
<Redirect from="/" to="/home" />
|
||||
<TabMenu />
|
||||
<Route exact path="/home">
|
||||
<Home />
|
||||
</Route>
|
||||
<Route exact path="/playlist/:id">
|
||||
<PlaylistView />
|
||||
</Route>
|
||||
<Route exact path="/genre/playlists/:id">
|
||||
<PlaylistGenreView />
|
||||
</Route>
|
||||
</>
|
||||
) : (
|
||||
<Login />
|
||||
)}
|
||||
</Route>
|
||||
<Route exact path="/playlist/:id">
|
||||
<PlaylistView />
|
||||
<Route path="/currently">
|
||||
<CurrentPlaylist />
|
||||
</Route>
|
||||
<Route exact path="/genre/playlists/:id">
|
||||
<PlaylistGenreView />
|
||||
<Route path="/library">
|
||||
<Library />
|
||||
</Route>
|
||||
</>
|
||||
) : (
|
||||
<Login />
|
||||
)}
|
||||
</Route>
|
||||
<Route path="/currently">
|
||||
<CurrentPlaylist />
|
||||
</Route>
|
||||
<Route path="/library">
|
||||
<Library />
|
||||
</Route>
|
||||
<Route exact path="/search">
|
||||
<Search />
|
||||
</Route>
|
||||
<Route exact path="/search/playlists">
|
||||
<SearchResultPlaylistCollection />
|
||||
</Route>
|
||||
<Route exact path="/search/songs">
|
||||
<SearchResultSongsCollection />
|
||||
</Route>
|
||||
<Route exact path="/settings/">
|
||||
<Settings />
|
||||
</Route>
|
||||
</>
|
||||
);
|
||||
<Route path="/artist">
|
||||
<Artist />
|
||||
</Route>
|
||||
<Route exact path="/search">
|
||||
<Search />
|
||||
</Route>
|
||||
<Route exact path="/search/playlists">
|
||||
<SearchResultPlaylistCollection />
|
||||
</Route>
|
||||
<Route exact path="/search/songs">
|
||||
<SearchResultSongsCollection />
|
||||
</Route>
|
||||
<Route exact path="/settings/">
|
||||
<Settings />
|
||||
</Route>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Routes;
|
||||
|
2090
tsconfig.tsbuildinfo
2090
tsconfig.tsbuildinfo
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user