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,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --mode=production",
|
"build": "webpack --mode=production",
|
||||||
"dev": "nodemon -w src/** -e ts,tsx -x 'node esbuild.config.mjs'",
|
"dev": "nodemon -w src/ -e ts,tsx -x 'node esbuild.config.mjs'",
|
||||||
"check-types": "tsc --noEmit --watch",
|
"check-types": "nodemon --quiet -e tsx,ts -w src/ -x tsc --noEmit --pretty",
|
||||||
"start": "cd dist && qode index.js",
|
"start": "cd dist && qode index.js",
|
||||||
"start:watch": "nodemon -w dist -e js -x \"npm start\"",
|
"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\"",
|
"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/spotify-web-api-node": "^5.0.0",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"@types/webpack-env": "^1.15.3",
|
"@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",
|
"@vitejs/plugin-react-refresh": "^1.3.3",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"concurrently": "^6.0.0",
|
"concurrently": "^6.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"esbuild": "^0.12.8",
|
"esbuild": "^0.12.8",
|
||||||
"esbuild-loader": "^2.13.1",
|
"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",
|
"file-loader": "^6.2.0",
|
||||||
"fork-ts-checker-webpack-plugin": "^6.2.0",
|
"fork-ts-checker-webpack-plugin": "^6.2.0",
|
||||||
"native-addon-loader": "^2.0.1",
|
"native-addon-loader": "^2.0.1",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
|
"prettier": "^2.3.1",
|
||||||
"typescript": "^4.2.3",
|
"typescript": "^4.2.3",
|
||||||
"webpack": "^5.27.0",
|
"webpack": "^5.27.0",
|
||||||
"webpack-cli": "^4.4.0"
|
"webpack-cli": "^4.4.0"
|
||||||
|
106
src/app.tsx
106
src/app.tsx
@ -1,6 +1,13 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { Window, hot, View } from "@nodegui/react-nodegui";
|
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 { MemoryRouter } from "react-router";
|
||||||
import Routes from "./routes";
|
import Routes from "./routes";
|
||||||
import { LocalStorage } from "node-localstorage";
|
import { LocalStorage } from "node-localstorage";
|
||||||
@ -16,7 +23,9 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { confDir, LocalStorageKeys } from "./conf";
|
import { confDir, LocalStorageKeys } from "./conf";
|
||||||
import spotubeIcon from "../assets/icon.svg";
|
import spotubeIcon from "../assets/icon.svg";
|
||||||
import preferencesContext, { PreferencesContextProperties } from "./context/preferencesContext";
|
import preferencesContext, {
|
||||||
|
PreferencesContextProperties,
|
||||||
|
} from "./context/preferencesContext";
|
||||||
|
|
||||||
export interface Credentials {
|
export interface Credentials {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
@ -77,30 +86,53 @@ function RootApp() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// saving changed credentials to storage
|
// saving changed credentials to storage
|
||||||
localStorage.setItem(LocalStorageKeys.credentials, JSON.stringify(credentials));
|
localStorage.setItem(LocalStorageKeys.credentials, JSON.stringify(credentials));
|
||||||
if (credentials.clientId && credentials.clientSecret && !localStorage.getItem(LocalStorageKeys.refresh_token)) {
|
if (
|
||||||
|
credentials.clientId &&
|
||||||
|
credentials.clientSecret &&
|
||||||
|
!localStorage.getItem(LocalStorageKeys.refresh_token)
|
||||||
|
) {
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
app.get<null, null, null, { code: string }>("/auth/spotify/callback", async (req, res) => {
|
app.get<null, null, null, { code: string }>(
|
||||||
|
"/auth/spotify/callback",
|
||||||
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
spotifyApi.setClientId(credentials.clientId);
|
spotifyApi.setClientId(credentials.clientId);
|
||||||
spotifyApi.setClientSecret(credentials.clientSecret);
|
spotifyApi.setClientSecret(credentials.clientSecret);
|
||||||
const { body: authRes } = await spotifyApi.authorizationCodeGrant(req.query.code);
|
const { body: authRes } = await spotifyApi.authorizationCodeGrant(
|
||||||
|
req.query.code,
|
||||||
|
);
|
||||||
setAccess_token(authRes.access_token);
|
setAccess_token(authRes.access_token);
|
||||||
localStorage.setItem(LocalStorageKeys.refresh_token, authRes.refresh_token);
|
localStorage.setItem(
|
||||||
|
LocalStorageKeys.refresh_token,
|
||||||
|
authRes.refresh_token,
|
||||||
|
);
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
return res.end();
|
return res.end();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fullfil code grant flow: ", error);
|
console.error("Failed to fullfil code grant flow: ", error);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const server = app.listen(4304, () => {
|
const server = app.listen(4304, () => {
|
||||||
console.log("Server is running");
|
console.log("Server is running");
|
||||||
spotifyApi.setClientId(credentials.clientId);
|
spotifyApi.setClientId(credentials.clientId);
|
||||||
spotifyApi.setClientSecret(credentials.clientSecret);
|
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) =>
|
open(
|
||||||
console.error("Opening IPC connection with browser failed: ", e)
|
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 () => {
|
return () => {
|
||||||
@ -118,7 +150,9 @@ function RootApp() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onWindowClose = () => {
|
const onWindowClose = () => {
|
||||||
if (audioPlayer.isRunning()) {
|
if (audioPlayer.isRunning()) {
|
||||||
audioPlayer.stop().catch((e) => console.error("Failed to quit MPV player: ", e));
|
audioPlayer
|
||||||
|
.stop()
|
||||||
|
.catch((e) => console.error("Failed to quit MPV player: ", e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -144,7 +178,11 @@ function RootApp() {
|
|||||||
|
|
||||||
async function spaceAction() {
|
async function spaceAction() {
|
||||||
try {
|
try {
|
||||||
currentTrack && audioPlayer.isRunning() && (await audioPlayer.isPaused()) ? await audioPlayer.play() : await audioPlayer.pause();
|
currentTrack &&
|
||||||
|
audioPlayer.isRunning() &&
|
||||||
|
(await audioPlayer.isPaused())
|
||||||
|
? await audioPlayer.play()
|
||||||
|
: await audioPlayer.pause();
|
||||||
console.log("You pressed SPACE");
|
console.log("You pressed SPACE");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error, "[Failed to play/pause audioPlayer]: ");
|
showError(error, "[Failed to play/pause audioPlayer]: ");
|
||||||
@ -152,7 +190,10 @@ function RootApp() {
|
|||||||
}
|
}
|
||||||
async function rightAction() {
|
async function rightAction() {
|
||||||
try {
|
try {
|
||||||
currentTrack && audioPlayer.isRunning() && (await audioPlayer.isSeekable()) && (await audioPlayer.seek(+5));
|
currentTrack &&
|
||||||
|
audioPlayer.isRunning() &&
|
||||||
|
(await audioPlayer.isSeekable()) &&
|
||||||
|
(await audioPlayer.seek(+5));
|
||||||
console.log("You pressed RIGHT");
|
console.log("You pressed RIGHT");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error, "[Failed to seek audioPlayer]: ");
|
showError(error, "[Failed to seek audioPlayer]: ");
|
||||||
@ -160,7 +201,10 @@ function RootApp() {
|
|||||||
}
|
}
|
||||||
async function leftAction() {
|
async function leftAction() {
|
||||||
try {
|
try {
|
||||||
currentTrack && audioPlayer.isRunning() && (await audioPlayer.isSeekable()) && (await audioPlayer.seek(-5));
|
currentTrack &&
|
||||||
|
audioPlayer.isRunning() &&
|
||||||
|
(await audioPlayer.isSeekable()) &&
|
||||||
|
(await audioPlayer.seek(-5));
|
||||||
console.log("You pressed LEFT");
|
console.log("You pressed LEFT");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error, "[Failed to seek audioPlayer]: ");
|
showError(error, "[Failed to seek audioPlayer]: ");
|
||||||
@ -183,13 +227,39 @@ function RootApp() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Window ref={windowRef} windowState={WindowState.WindowMaximized} windowIcon={winIcon} windowTitle="Spotube" minSize={minSize}>
|
<Window
|
||||||
|
ref={windowRef}
|
||||||
|
windowState={WindowState.WindowMaximized}
|
||||||
|
windowIcon={winIcon}
|
||||||
|
windowTitle="Spotube"
|
||||||
|
minSize={minSize}
|
||||||
|
>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, setAccess_token, ...credentials, setCredentials }}>
|
<authContext.Provider
|
||||||
<preferencesContext.Provider value={{ ...preferences, setPreferences }}>
|
value={{
|
||||||
<playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}>
|
isLoggedIn,
|
||||||
|
setIsLoggedIn,
|
||||||
|
access_token,
|
||||||
|
setAccess_token,
|
||||||
|
...credentials,
|
||||||
|
setCredentials,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<preferencesContext.Provider
|
||||||
|
value={{ ...preferences, setPreferences }}
|
||||||
|
>
|
||||||
|
<playerContext.Provider
|
||||||
|
value={{
|
||||||
|
currentPlaylist,
|
||||||
|
currentTrack,
|
||||||
|
setCurrentPlaylist,
|
||||||
|
setCurrentTrack,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<View style={`flex: 1; flex-direction: 'column'; align-items: 'stretch';`}>
|
<View
|
||||||
|
style={`flex: 1; flex-direction: 'column'; align-items: 'stretch';`}
|
||||||
|
>
|
||||||
<Routes />
|
<Routes />
|
||||||
{isLoggedIn && <Player />}
|
{isLoggedIn && <Player />}
|
||||||
</View>
|
</View>
|
||||||
|
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;
|
@ -7,7 +7,13 @@ import IconButton from "./shared/IconButton";
|
|||||||
function BackButton(): ReactElement {
|
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;
|
export default BackButton;
|
||||||
|
@ -8,13 +8,15 @@ function CurrentPlaylist() {
|
|||||||
const { currentPlaylist, currentTrack } = useContext(playerContext);
|
const { currentPlaylist, currentTrack } = useContext(playerContext);
|
||||||
|
|
||||||
if (!currentPlaylist && !currentTrack) {
|
if (!currentPlaylist && !currentTrack) {
|
||||||
return <Text style="flex: 1;">{`<center>There is nothing being played now</center>`}</Text>;
|
return (
|
||||||
|
<Text style="flex: 1;">{`<center>There is nothing being played now</center>`}</Text>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentTrack && !currentPlaylist) {
|
if (currentTrack && !currentPlaylist) {
|
||||||
<View style="flex: 1;">
|
<View style="flex: 1;">
|
||||||
<TrackButton track={currentTrack} index={0}/>
|
<TrackButton track={currentTrack} index={0} />
|
||||||
</View>
|
</View>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -24,7 +26,13 @@ function CurrentPlaylist() {
|
|||||||
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
||||||
<View style={`flex-direction:column; flex: 1;`}>
|
<View style={`flex-direction:column; flex: 1;`}>
|
||||||
{currentPlaylist?.tracks.map(({ track }, index) => {
|
{currentPlaylist?.tracks.map(({ track }, index) => {
|
||||||
return <TrackButton key={index + track.id} track={track} index={index} />;
|
return (
|
||||||
|
<TrackButton
|
||||||
|
key={index + track.id}
|
||||||
|
track={track}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
@ -7,16 +7,27 @@ import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
|||||||
import CategoryCardView from "./shared/CategoryCardView";
|
import CategoryCardView from "./shared/CategoryCardView";
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const { data: pagedCategories, isError, refetch, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } = useSpotifyInfiniteQuery<SpotifyApi.PagingObject<SpotifyApi.CategoryObject>>(
|
const {
|
||||||
|
data: pagedCategories,
|
||||||
|
isError,
|
||||||
|
refetch,
|
||||||
|
isLoading,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
} = useSpotifyInfiniteQuery<SpotifyApi.PagingObject<SpotifyApi.CategoryObject>>(
|
||||||
QueryCacheKeys.categories,
|
QueryCacheKeys.categories,
|
||||||
(spotifyApi, { pageParam }) => spotifyApi.getCategories({ country: "US", limit: 10, offset: pageParam }).then((categoriesReceived) => categoriesReceived.body.categories),
|
(spotifyApi, { pageParam }) =>
|
||||||
|
spotifyApi
|
||||||
|
.getCategories({ country: "US", limit: 10, offset: pageParam })
|
||||||
|
.then((categoriesReceived) => categoriesReceived.body.categories),
|
||||||
{
|
{
|
||||||
getNextPageParam(lastPage) {
|
getNextPageParam(lastPage) {
|
||||||
if (lastPage.next) {
|
if (lastPage.next) {
|
||||||
return lastPage.offset + lastPage.limit;
|
return lastPage.offset + lastPage.limit;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const categories = pagedCategories?.pages
|
const categories = pagedCategories?.pages
|
||||||
@ -27,11 +38,29 @@ function Home() {
|
|||||||
return (
|
return (
|
||||||
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
||||||
<View style={`flex-direction: 'column'; align-items: 'center'; flex: 1;`}>
|
<View style={`flex-direction: 'column'; align-items: 'center'; flex: 1;`}>
|
||||||
<PlaceholderApplet error={isError} message="Failed to query genres" reload={refetch} helps loading={isLoading} />
|
<PlaceholderApplet
|
||||||
|
error={isError}
|
||||||
|
message="Failed to query genres"
|
||||||
|
reload={refetch}
|
||||||
|
helps
|
||||||
|
loading={isLoading}
|
||||||
|
/>
|
||||||
{categories?.map((category, index) => {
|
{categories?.map((category, index) => {
|
||||||
return <CategoryCard key={index + category.id} id={category.id} name={category.name} />;
|
return (
|
||||||
|
<CategoryCard
|
||||||
|
key={index + category.id}
|
||||||
|
id={category.id}
|
||||||
|
name={category.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
{hasNextPage && <Button on={{ clicked: () => fetchNextPage() }} text="Load More" enabled={!isFetchingNextPage} />}
|
{hasNextPage && (
|
||||||
|
<Button
|
||||||
|
on={{ clicked: () => fetchNextPage() }}
|
||||||
|
text="Load More"
|
||||||
|
enabled={!isFetchingNextPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
@ -45,7 +74,9 @@ interface CategoryCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CategoryCard = ({ id, name }: CategoryCardProps) => {
|
const CategoryCard = ({ id, name }: CategoryCardProps) => {
|
||||||
const { data: playlists, isError } = useSpotifyQuery<SpotifyApi.PlaylistObjectSimplified[]>(
|
const { data: playlists, isError } = useSpotifyQuery<
|
||||||
|
SpotifyApi.PlaylistObjectSimplified[]
|
||||||
|
>(
|
||||||
[QueryCacheKeys.categoryPlaylists, id],
|
[QueryCacheKeys.categoryPlaylists, id],
|
||||||
async (spotifyApi) => {
|
async (spotifyApi) => {
|
||||||
const option = { limit: 4 };
|
const option = { limit: 4 };
|
||||||
@ -57,8 +88,15 @@ const CategoryCard = ({ id, name }: CategoryCardProps) => {
|
|||||||
}
|
}
|
||||||
return res.body.playlists.items;
|
return res.body.playlists.items;
|
||||||
},
|
},
|
||||||
{ initialData: [] }
|
{ 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 React, { useContext } from "react";
|
||||||
import { Redirect, Route } from "react-router";
|
import { Redirect, Route } from "react-router";
|
||||||
import { QueryCacheKeys } from "../conf";
|
import { QueryCacheKeys } from "../conf";
|
||||||
@ -6,6 +7,7 @@ import playerContext from "../context/playerContext";
|
|||||||
import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
||||||
import { GenreView } from "./PlaylistGenreView";
|
import { GenreView } from "./PlaylistGenreView";
|
||||||
import { PlaylistSimpleControls, TrackTableIndex } from "./PlaylistView";
|
import { PlaylistSimpleControls, TrackTableIndex } from "./PlaylistView";
|
||||||
|
import CachedImage from "./shared/CachedImage";
|
||||||
import PlaceholderApplet from "./shared/PlaceholderApplet";
|
import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||||
import { TrackButton, TrackButtonPlaylistObject } from "./shared/TrackButton";
|
import { TrackButton, TrackButtonPlaylistObject } from "./shared/TrackButton";
|
||||||
import { TabMenuItem } from "./TabMenu";
|
import { TabMenuItem } from "./TabMenu";
|
||||||
@ -17,6 +19,7 @@ function Library() {
|
|||||||
<View style="max-width: 350px; justify-content: 'space-evenly'">
|
<View style="max-width: 350px; justify-content: 'space-evenly'">
|
||||||
<TabMenuItem title="Saved Tracks" url="/library/saved-tracks" />
|
<TabMenuItem title="Saved Tracks" url="/library/saved-tracks" />
|
||||||
<TabMenuItem title="Playlists" url="/library/playlists" />
|
<TabMenuItem title="Playlists" url="/library/playlists" />
|
||||||
|
<TabMenuItem title="Artists" url="/library/followed-artists" />
|
||||||
</View>
|
</View>
|
||||||
<Route exact path="/library/saved-tracks">
|
<Route exact path="/library/saved-tracks">
|
||||||
<UserSavedTracks />
|
<UserSavedTracks />
|
||||||
@ -24,6 +27,9 @@ function Library() {
|
|||||||
<Route exact path="/library/playlists">
|
<Route exact path="/library/playlists">
|
||||||
<UserPlaylists />
|
<UserPlaylists />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path="/library/followed-artists">
|
||||||
|
<FollowedArtists />
|
||||||
|
</Route>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -31,10 +37,20 @@ function Library() {
|
|||||||
export default Library;
|
export default Library;
|
||||||
|
|
||||||
function UserPlaylists() {
|
function UserPlaylists() {
|
||||||
const { data: userPagedPlaylists, isError, isLoading, refetch, isFetchingNextPage, hasNextPage, fetchNextPage } = useSpotifyInfiniteQuery<SpotifyApi.ListOfUsersPlaylistsResponse>(
|
const {
|
||||||
|
data: userPagedPlaylists,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
isFetchingNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
} = useSpotifyInfiniteQuery<SpotifyApi.ListOfUsersPlaylistsResponse>(
|
||||||
QueryCacheKeys.userPlaylists,
|
QueryCacheKeys.userPlaylists,
|
||||||
(spotifyApi, { pageParam }) =>
|
(spotifyApi, { pageParam }) =>
|
||||||
spotifyApi.getUserPlaylists({ limit: 20, offset: pageParam }).then((userPlaylists) => {
|
spotifyApi
|
||||||
|
.getUserPlaylists({ limit: 20, offset: pageParam })
|
||||||
|
.then((userPlaylists) => {
|
||||||
return userPlaylists.body;
|
return userPlaylists.body;
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@ -43,7 +59,7 @@ function UserPlaylists() {
|
|||||||
return lastPage.offset + lastPage.limit;
|
return lastPage.offset + lastPage.limit;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const userPlaylists = userPagedPlaylists?.pages
|
const userPlaylists = userPagedPlaylists?.pages
|
||||||
@ -59,25 +75,37 @@ function UserPlaylists() {
|
|||||||
playlists={userPlaylists ?? []}
|
playlists={userPlaylists ?? []}
|
||||||
isLoadable={!isFetchingNextPage}
|
isLoadable={!isFetchingNextPage}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
loadMore={hasNextPage ? ()=>fetchNextPage() : undefined}
|
loadMore={hasNextPage ? () => fetchNextPage() : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserSavedTracks() {
|
function UserSavedTracks() {
|
||||||
const userSavedPlaylistId = "user-saved-tracks";
|
const userSavedPlaylistId = "user-saved-tracks";
|
||||||
const { data: userSavedTracks, fetchNextPage, hasNextPage, isFetchingNextPage, isError, isLoading, refetch } = useSpotifyInfiniteQuery<SpotifyApi.UsersSavedTracksResponse>(
|
const {
|
||||||
|
data: userSavedTracks,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
} = useSpotifyInfiniteQuery<SpotifyApi.UsersSavedTracksResponse>(
|
||||||
QueryCacheKeys.userSavedTracks,
|
QueryCacheKeys.userSavedTracks,
|
||||||
(spotifyApi, { pageParam }) => spotifyApi.getMySavedTracks({ limit: 50, offset: pageParam }).then((res) => res.body),
|
(spotifyApi, { pageParam }) =>
|
||||||
|
spotifyApi
|
||||||
|
.getMySavedTracks({ limit: 50, offset: pageParam })
|
||||||
|
.then((res) => res.body),
|
||||||
{
|
{
|
||||||
getNextPageParam(lastPage) {
|
getNextPageParam(lastPage) {
|
||||||
if (lastPage.next) {
|
if (lastPage.next) {
|
||||||
return lastPage.offset + lastPage.limit;
|
return lastPage.offset + lastPage.limit;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack } = useContext(playerContext);
|
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack } =
|
||||||
|
useContext(playerContext);
|
||||||
|
|
||||||
const userTracks = userSavedTracks?.pages
|
const userTracks = userSavedTracks?.pages
|
||||||
?.map((page) => page.items)
|
?.map((page) => page.items)
|
||||||
@ -86,7 +114,13 @@ function UserSavedTracks() {
|
|||||||
|
|
||||||
function handlePlaylistPlayPause(index?: number) {
|
function handlePlaylistPlayPause(index?: number) {
|
||||||
if (currentPlaylist?.id !== userSavedPlaylistId && userTracks) {
|
if (currentPlaylist?.id !== userSavedPlaylistId && userTracks) {
|
||||||
setCurrentPlaylist({ id: userSavedPlaylistId, name: "Liked Tracks", thumbnail: "https://nerdist.com/wp-content/uploads/2020/07/maxresdefault.jpg", tracks: 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);
|
setCurrentTrack(userTracks[index ?? 0].track);
|
||||||
} else {
|
} else {
|
||||||
setCurrentPlaylist(undefined);
|
setCurrentPlaylist(undefined);
|
||||||
@ -111,7 +145,15 @@ function UserSavedTracks() {
|
|||||||
id: userSavedPlaylistId,
|
id: userSavedPlaylistId,
|
||||||
images: [{ url: "https://facebook.com/img.jpeg" }],
|
images: [{ url: "https://facebook.com/img.jpeg" }],
|
||||||
name: "User saved track",
|
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 } },
|
owner: {
|
||||||
|
external_urls: { spotify: "" },
|
||||||
|
href: "",
|
||||||
|
id: "Me",
|
||||||
|
type: "user",
|
||||||
|
uri: "spotify:user:me",
|
||||||
|
display_name: "User",
|
||||||
|
followers: { href: null, total: 0 },
|
||||||
|
},
|
||||||
public: false,
|
public: false,
|
||||||
snapshot_id: userSavedPlaylistId + "snapshot",
|
snapshot_id: userSavedPlaylistId + "snapshot",
|
||||||
type: "playlist",
|
type: "playlist",
|
||||||
@ -119,12 +161,30 @@ function UserSavedTracks() {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<View style="flex: 1; flex-direction: 'column';">
|
<View style="flex: 1; flex-direction: 'column';">
|
||||||
<PlaylistSimpleControls handlePlaylistPlayPause={handlePlaylistPlayPause} isActive={currentPlaylist?.id === userSavedPlaylistId} />
|
<PlaylistSimpleControls
|
||||||
|
handlePlaylistPlayPause={handlePlaylistPlayPause}
|
||||||
|
isActive={currentPlaylist?.id === userSavedPlaylistId}
|
||||||
|
/>
|
||||||
<TrackTableIndex />
|
<TrackTableIndex />
|
||||||
<ScrollArea style="flex: 1; border: none;">
|
<ScrollArea style="flex: 1; border: none;">
|
||||||
<View style="flex: 1; flex-direction: 'column'; align-items: 'stretch';">
|
<View style="flex: 1; flex-direction: 'column'; align-items: 'stretch';">
|
||||||
<PlaceholderApplet error={isError} loading={isLoading || isFetchingNextPage} message="Failed querying spotify" reload={refetch} />
|
<PlaceholderApplet
|
||||||
{userTracks?.map(({ track }, index) => track && <TrackButton key={index + track.id} track={track} index={index} playlist={playlist} />)}
|
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 && (
|
{hasNextPage && (
|
||||||
<Button
|
<Button
|
||||||
style="flex-grow: 0; align-self: 'center';"
|
style="flex-grow: 0; align-self: 'center';"
|
||||||
@ -142,3 +202,84 @@ function UserSavedTracks() {
|
|||||||
</View>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,15 +1,23 @@
|
|||||||
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 React, { PropsWithChildren, useEffect, useState } from "react";
|
||||||
import showError from "../helpers/showError";
|
import showError from "../helpers/showError";
|
||||||
import fetchLyrics from "../helpers/fetchLyrics";
|
import fetchLyrics from "../helpers/fetchLyrics";
|
||||||
|
|
||||||
interface ManualLyricDialogProps extends PropsWithChildren<{}> {
|
interface ManualLyricDialogProps extends PropsWithChildren<unknown> {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose?: (closed: boolean) => void;
|
onClose?: (closed: boolean) => void;
|
||||||
track: SpotifyApi.TrackObjectSimplified | SpotifyApi.TrackObjectFull;
|
track: SpotifyApi.TrackObjectSimplified | SpotifyApi.TrackObjectFull;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManualLyricDialog({ open, track, onClose }: ManualLyricDialogProps) {
|
function ManualLyricDialog({ open, track }: ManualLyricDialogProps) {
|
||||||
const dialog = new QDialog();
|
const dialog = new QDialog();
|
||||||
const areaContainer = new QWidget();
|
const areaContainer = new QWidget();
|
||||||
const retryButton = new QPushButton();
|
const retryButton = new QPushButton();
|
||||||
@ -23,7 +31,7 @@ function ManualLyricDialog({ open, track, onClose }: ManualLyricDialogProps) {
|
|||||||
async function handleBtnClick() {
|
async function handleBtnClick() {
|
||||||
try {
|
try {
|
||||||
const lyrics = await fetchLyrics(artists, track.name);
|
const lyrics = await fetchLyrics(artists, track.name);
|
||||||
console.log('lyrics:', lyrics)
|
console.log("lyrics:", lyrics);
|
||||||
setLyrics(lyrics);
|
setLyrics(lyrics);
|
||||||
setLyricNotFound(lyrics === "Not Found");
|
setLyricNotFound(lyrics === "Not Found");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,12 +1,31 @@
|
|||||||
import { Direction, Orientation, QAbstractSliderSignals, QIcon } from "@nodegui/nodegui";
|
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 React, { ReactElement, useContext, useEffect, useState } from "react";
|
||||||
import playerContext, { CurrentPlaylist } from "../context/playerContext";
|
import playerContext, { CurrentPlaylist } from "../context/playerContext";
|
||||||
import { shuffleArray } from "../helpers/shuffleArray";
|
import { shuffleArray } from "../helpers/shuffleArray";
|
||||||
import NodeMpv from "node-mpv";
|
import NodeMpv from "node-mpv";
|
||||||
import { getYoutubeTrack, YoutubeTrack } from "../helpers/getYoutubeTrack";
|
import { getYoutubeTrack, YoutubeTrack } from "../helpers/getYoutubeTrack";
|
||||||
import PlayerProgressBar from "./PlayerProgressBar";
|
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 IconButton from "./shared/IconButton";
|
||||||
import showError from "../helpers/showError";
|
import showError from "../helpers/showError";
|
||||||
import useTrackReaction from "../hooks/useTrackReaction";
|
import useTrackReaction from "../hooks/useTrackReaction";
|
||||||
@ -23,16 +42,19 @@ export const audioPlayer = new NodeMpv(
|
|||||||
// debug: true,
|
// debug: true,
|
||||||
// verbose: true,
|
// verbose: true,
|
||||||
},
|
},
|
||||||
["--ytdl-raw-options-set=format=140,http-chunk-size=300000"]
|
["--ytdl-raw-options-set=format=140,http-chunk-size=300000"],
|
||||||
);
|
);
|
||||||
function Player(): ReactElement {
|
function Player(): ReactElement {
|
||||||
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
|
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } =
|
||||||
|
useContext(playerContext);
|
||||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||||
|
|
||||||
const cachedVolume = localStorage.getItem(LocalStorageKeys.volume);
|
const cachedVolume = localStorage.getItem(LocalStorageKeys.volume);
|
||||||
|
|
||||||
const [isPaused, setIsPaused] = useState(true);
|
const [isPaused, setIsPaused] = useState(true);
|
||||||
const [volume, setVolume] = useState<number>(() => (cachedVolume ? parseFloat(cachedVolume) : 55));
|
const [volume, setVolume] = useState<number>(() =>
|
||||||
|
cachedVolume ? parseFloat(cachedVolume) : 55,
|
||||||
|
);
|
||||||
const [totalDuration, setTotalDuration] = useState<number>(0);
|
const [totalDuration, setTotalDuration] = useState<number>(0);
|
||||||
const [shuffle, setShuffle] = useState<boolean>(false);
|
const [shuffle, setShuffle] = useState<boolean>(false);
|
||||||
const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]);
|
const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]);
|
||||||
@ -51,7 +73,7 @@ function Player(): ReactElement {
|
|||||||
localStorage.setItem(LocalStorageKeys.volume, volume.toString());
|
localStorage.setItem(LocalStorageKeys.volume, volume.toString());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[volume]
|
[volume],
|
||||||
);
|
);
|
||||||
const playerRunning = audioPlayer.isRunning();
|
const playerRunning = audioPlayer.isRunning();
|
||||||
const cachedPlaylist = localStorage.getItem(LocalStorageKeys.cachedPlaylist);
|
const cachedPlaylist = localStorage.getItem(LocalStorageKeys.cachedPlaylist);
|
||||||
@ -87,8 +109,12 @@ function Player(): ReactElement {
|
|||||||
// track change effect
|
// track change effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// caching current track
|
// caching current track
|
||||||
if(!currentTrack) localStorage.removeItem(LocalStorageKeys.cachedTrack);
|
if (!currentTrack) localStorage.removeItem(LocalStorageKeys.cachedTrack);
|
||||||
else localStorage.setItem(LocalStorageKeys.cachedTrack, JSON.stringify(currentTrack));
|
else
|
||||||
|
localStorage.setItem(
|
||||||
|
LocalStorageKeys.cachedTrack,
|
||||||
|
JSON.stringify(currentTrack),
|
||||||
|
);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@ -114,8 +140,12 @@ function Player(): ReactElement {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShuffle(false);
|
setShuffle(false);
|
||||||
// caching playlist
|
// caching playlist
|
||||||
if(!currentPlaylist) localStorage.removeItem(LocalStorageKeys.cachedPlaylist);
|
if (!currentPlaylist) localStorage.removeItem(LocalStorageKeys.cachedPlaylist);
|
||||||
else localStorage.setItem(LocalStorageKeys.cachedPlaylist, JSON.stringify(currentPlaylist));
|
else
|
||||||
|
localStorage.setItem(
|
||||||
|
LocalStorageKeys.cachedPlaylist,
|
||||||
|
JSON.stringify(currentPlaylist),
|
||||||
|
);
|
||||||
}, [currentPlaylist]);
|
}, [currentPlaylist]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -140,6 +170,7 @@ function Player(): ReactElement {
|
|||||||
// live Effect
|
// live Effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (playerRunning) {
|
if (playerRunning) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const statusListener = (status: { property: string; value: any }) => {
|
const statusListener = (status: { property: string; value: any }) => {
|
||||||
if (status?.property === "duration") {
|
if (status?.property === "duration") {
|
||||||
setTotalDuration(status.value ?? 0);
|
setTotalDuration(status.value ?? 0);
|
||||||
@ -149,9 +180,17 @@ function Player(): ReactElement {
|
|||||||
setIsStopped(true);
|
setIsStopped(true);
|
||||||
setIsPaused(true);
|
setIsPaused(true);
|
||||||
// go to next track
|
// go to next track
|
||||||
if (currentTrack && playlistTracksIds && currentPlaylist?.tracks.length !== 0) {
|
if (
|
||||||
|
currentTrack &&
|
||||||
|
playlistTracksIds &&
|
||||||
|
currentPlaylist?.tracks.length !== 0
|
||||||
|
) {
|
||||||
const index = playlistTracksIds?.indexOf(currentTrack.id) + 1;
|
const index = playlistTracksIds?.indexOf(currentTrack.id) + 1;
|
||||||
setCurrentTrack(currentPlaylist?.tracks[index > playlistTracksIds.length - 1 ? 0 : index].track);
|
setCurrentTrack(
|
||||||
|
currentPlaylist?.tracks[
|
||||||
|
index > playlistTracksIds.length - 1 ? 0 : index
|
||||||
|
].track,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const pauseListener = () => {
|
const pauseListener = () => {
|
||||||
@ -192,7 +231,15 @@ function Player(): ReactElement {
|
|||||||
const prevOrNext = (constant: number) => {
|
const prevOrNext = (constant: number) => {
|
||||||
if (currentTrack && playlistTracksIds && currentPlaylist) {
|
if (currentTrack && playlistTracksIds && currentPlaylist) {
|
||||||
const index = playlistTracksIds.indexOf(currentTrack.id) + constant;
|
const index = playlistTracksIds.indexOf(currentTrack.id) + constant;
|
||||||
setCurrentTrack(currentPlaylist.tracks[index > playlistTracksIds?.length - 1 ? 0 : index < 0 ? playlistTracksIds.length - 1 : index].track);
|
setCurrentTrack(
|
||||||
|
currentPlaylist.tracks[
|
||||||
|
index > playlistTracksIds?.length - 1
|
||||||
|
? 0
|
||||||
|
: index < 0
|
||||||
|
? playlistTracksIds.length - 1
|
||||||
|
: index
|
||||||
|
].track,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -216,29 +263,71 @@ function Player(): ReactElement {
|
|||||||
<Text wordWrap openExternalLinks>
|
<Text wordWrap openExternalLinks>
|
||||||
{artistsNames && currentTrack
|
{artistsNames && currentTrack
|
||||||
? `
|
? `
|
||||||
<p><b><a href="${currentYtTrack?.youtube_uri}"}>${currentTrack.name}</a></b> - ${artistsNames[0]} ${artistsNames.length > 1 ? "feat. " + artistsNames.slice(1).join(", ") : ""}</p>
|
<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>`}
|
: `<b>Oh, dear don't waste time</b>`}
|
||||||
</Text>
|
</Text>
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
<GridColumn width={4}>
|
<GridColumn width={4}>
|
||||||
<BoxView direction={Direction.TopToBottom} style={`max-width: 600px; min-width: 380px;`}>
|
<BoxView
|
||||||
{currentTrack && <ManualLyricDialog open={openLyrics} track={currentTrack} />}
|
direction={Direction.TopToBottom}
|
||||||
<PlayerProgressBar audioPlayer={audioPlayer} totalDuration={totalDuration} />
|
style={`max-width: 600px; min-width: 380px;`}
|
||||||
|
>
|
||||||
|
{currentTrack && (
|
||||||
|
<ManualLyricDialog open={openLyrics} track={currentTrack} />
|
||||||
|
)}
|
||||||
|
<PlayerProgressBar
|
||||||
|
audioPlayer={audioPlayer}
|
||||||
|
totalDuration={totalDuration}
|
||||||
|
/>
|
||||||
|
|
||||||
<BoxView direction={Direction.LeftToRight}>
|
<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
|
||||||
<IconButton on={{ clicked: () => prevOrNext(-1) }} icon={new QIcon(backward)} />
|
style={`background-color: ${
|
||||||
<IconButton on={{ clicked: handlePlayPause }} icon={new QIcon(isStopped || isPaused || !currentTrack ? play : pause)} />
|
shuffle ? "orange" : "rgba(255, 255, 255, 0.055)"
|
||||||
<IconButton on={{ clicked: () => prevOrNext(1) }} icon={new QIcon(forward)} />
|
}`}
|
||||||
<IconButton icon={new QIcon(stop)} on={{ clicked: stopPlayback }} />
|
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>
|
||||||
</BoxView>
|
</BoxView>
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
<GridColumn width={2}>
|
<GridColumn width={2}>
|
||||||
<BoxView>
|
<BoxView>
|
||||||
<IconButton
|
<IconButton
|
||||||
style={isActiveDownloading() && !isFinishedDownloading() ? "background-color: green;" : ""}
|
style={
|
||||||
|
isActiveDownloading() && !isFinishedDownloading()
|
||||||
|
? "background-color: green;"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
enabled={!!currentYtTrack}
|
enabled={!!currentYtTrack}
|
||||||
icon={new QIcon(download)}
|
icon={new QIcon(download)}
|
||||||
on={{
|
on={{
|
||||||
@ -251,18 +340,36 @@ function Player(): ReactElement {
|
|||||||
on={{
|
on={{
|
||||||
clicked() {
|
clicked() {
|
||||||
if (currentTrack) {
|
if (currentTrack) {
|
||||||
reactToTrack({ added_at: Date.now().toString(), track: currentTrack });
|
reactToTrack({
|
||||||
|
added_at: Date.now().toString(),
|
||||||
|
track: currentTrack,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
icon={new QIcon(isFavorite(currentTrack?.id ?? "") ? heart : heartRegular)}
|
icon={
|
||||||
|
new QIcon(
|
||||||
|
isFavorite(currentTrack?.id ?? "")
|
||||||
|
? heart
|
||||||
|
: heartRegular,
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
style={openLyrics ? "background-color: green;" : ""}
|
style={openLyrics ? "background-color: green;" : ""}
|
||||||
icon={new QIcon(musicNode)}
|
icon={new QIcon(musicNode)}
|
||||||
on={{ clicked: () => currentTrack && setOpenLyrics(!openLyrics) }}
|
on={{
|
||||||
|
clicked: () => currentTrack && setOpenLyrics(!openLyrics),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
minSize={{ height: 20, width: 80 }}
|
||||||
|
maxSize={{ height: 20, width: 100 }}
|
||||||
|
hasTracking
|
||||||
|
sliderPosition={volume}
|
||||||
|
on={volumeHandler}
|
||||||
|
orientation={Orientation.Horizontal}
|
||||||
/>
|
/>
|
||||||
<Slider minSize={{ height: 20, width: 80 }} maxSize={{ height: 20, width: 100 }} hasTracking sliderPosition={volume} on={volumeHandler} orientation={Orientation.Horizontal} />
|
|
||||||
</BoxView>
|
</BoxView>
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
|
@ -30,7 +30,7 @@ function PlayerProgressBar({ audioPlayer, totalDuration }: PlayerProgressBarProp
|
|||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[currentTrack, totalDuration, trackTime]
|
[currentTrack, totalDuration, trackTime],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -50,10 +50,22 @@ function PlayerProgressBar({ audioPlayer, totalDuration }: PlayerProgressBarProp
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
const playbackPercentage = totalDuration > 0 ? (trackTime * 100) / totalDuration : 0;
|
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)
|
const playbackTime =
|
||||||
|
new Date(trackTime * 1000).toISOString().substr(14, 5) +
|
||||||
|
"/" +
|
||||||
|
new Date(totalDuration * 1000).toISOString().substr(14, 5);
|
||||||
return (
|
return (
|
||||||
<BoxView direction={Direction.LeftToRight} style={`padding: 20px 0px; flex-direction: row;`}>
|
<BoxView
|
||||||
<Slider enabled={!!currentTrack || trackTime > 0} on={trackSliderEvents} sliderPosition={playbackPercentage} hasTracking orientation={Orientation.Horizontal} />
|
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>
|
<Text>{playbackTime}</Text>
|
||||||
</BoxView>
|
</BoxView>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { QAbstractButtonSignals } from "@nodegui/nodegui";
|
import { QAbstractButtonSignals } from "@nodegui/nodegui";
|
||||||
import { Button, ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
import { Button, ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { RefetchOptions } from "react-query";
|
||||||
import { useLocation, useParams } from "react-router";
|
import { useLocation, useParams } from "react-router";
|
||||||
import { QueryCacheKeys } from "../conf";
|
import { QueryCacheKeys } from "../conf";
|
||||||
import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
||||||
@ -11,7 +12,17 @@ import PlaylistCard from "./shared/PlaylistCard";
|
|||||||
function PlaylistGenreView() {
|
function PlaylistGenreView() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const location = useLocation<{ name: string }>();
|
const location = useLocation<{ name: string }>();
|
||||||
const { data: pagedPlaylists, isError, isLoading, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = useSpotifyInfiniteQuery<SpotifyApi.PagingObject<SpotifyApi.PlaylistObjectSimplified>>(
|
const {
|
||||||
|
data: pagedPlaylists,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
} = useSpotifyInfiniteQuery<
|
||||||
|
SpotifyApi.PagingObject<SpotifyApi.PlaylistObjectSimplified>
|
||||||
|
>(
|
||||||
[QueryCacheKeys.genrePlaylists, id],
|
[QueryCacheKeys.genrePlaylists, id],
|
||||||
async (spotifyApi, { pageParam }) => {
|
async (spotifyApi, { pageParam }) => {
|
||||||
const option = { limit: 20, offset: pageParam };
|
const option = { limit: 20, offset: pageParam };
|
||||||
@ -29,7 +40,7 @@ function PlaylistGenreView() {
|
|||||||
return lastPage.offset + lastPage.limit;
|
return lastPage.offset + lastPage.limit;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const playlists = pagedPlaylists?.pages
|
const playlists = pagedPlaylists?.pages
|
||||||
@ -59,10 +70,18 @@ interface GenreViewProps {
|
|||||||
isLoadable?: boolean;
|
isLoadable?: boolean;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
refetch: Function;
|
refetch: (options?: RefetchOptions | undefined) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GenreView({ heading, playlists, loadMore, isLoadable, isError, isLoading, refetch }: GenreViewProps) {
|
export function GenreView({
|
||||||
|
heading,
|
||||||
|
playlists,
|
||||||
|
loadMore,
|
||||||
|
isLoadable,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
}: GenreViewProps) {
|
||||||
const playlistGenreViewStylesheet = `
|
const playlistGenreViewStylesheet = `
|
||||||
#genre-container{
|
#genre-container{
|
||||||
flex-direction: 'column';
|
flex-direction: 'column';
|
||||||
@ -89,11 +108,24 @@ export function GenreView({ heading, playlists, loadMore, isLoadable, isError, i
|
|||||||
<Text id="heading">{`<h2>${heading}</h2>`}</Text>
|
<Text id="heading">{`<h2>${heading}</h2>`}</Text>
|
||||||
<ScrollArea id="scroll-view">
|
<ScrollArea id="scroll-view">
|
||||||
<View id="child-container">
|
<View id="child-container">
|
||||||
<PlaceholderApplet error={isError} loading={isLoading} reload={refetch} message={`Failed loading ${heading}'s playlists`} />
|
<PlaceholderApplet
|
||||||
|
error={isError}
|
||||||
|
loading={isLoading}
|
||||||
|
reload={refetch}
|
||||||
|
message={`Failed loading ${heading}'s playlists`}
|
||||||
|
/>
|
||||||
{playlists?.map((playlist, index) => {
|
{playlists?.map((playlist, index) => {
|
||||||
return <PlaylistCard key={index + playlist.id} playlist={playlist} />;
|
return (
|
||||||
|
<PlaylistCard key={index + playlist.id} playlist={playlist} />
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
{loadMore && <Button text="Load more" on={{ clicked: loadMore }} enabled={isLoadable} />}
|
{loadMore && (
|
||||||
|
<Button
|
||||||
|
text="Load more"
|
||||||
|
on={{ clicked: loadMore }}
|
||||||
|
enabled={isLoadable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</View>
|
</View>
|
||||||
|
@ -20,17 +20,27 @@ export interface PlaylistTrackRes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PlaylistView: FC = () => {
|
const PlaylistView: FC = () => {
|
||||||
const { setCurrentTrack, currentPlaylist, setCurrentPlaylist } = useContext(playerContext);
|
const { setCurrentTrack, currentPlaylist, setCurrentPlaylist } =
|
||||||
|
useContext(playerContext);
|
||||||
const params = useParams<{ id: string }>();
|
const params = useParams<{ id: string }>();
|
||||||
const location = useLocation<{ name: string; thumbnail: string }>();
|
const location = useLocation<{ name: string; thumbnail: string }>();
|
||||||
const { isFavorite, reactToPlaylist } = usePlaylistReaction();
|
const { isFavorite, reactToPlaylist } = usePlaylistReaction();
|
||||||
const { data: playlist } = useSpotifyQuery<SpotifyApi.PlaylistObjectFull>([QueryCacheKeys.categoryPlaylists, params.id], (spotifyApi) =>
|
const { data: playlist } = useSpotifyQuery<SpotifyApi.PlaylistObjectFull>(
|
||||||
spotifyApi.getPlaylist(params.id).then((playlistsRes) => playlistsRes.body)
|
[QueryCacheKeys.categoryPlaylists, params.id],
|
||||||
|
(spotifyApi) =>
|
||||||
|
spotifyApi.getPlaylist(params.id).then((playlistsRes) => playlistsRes.body),
|
||||||
);
|
);
|
||||||
const { data: tracks, isSuccess, isError, isLoading, refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>(
|
const {
|
||||||
|
data: tracks,
|
||||||
|
isSuccess,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
} = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>(
|
||||||
[QueryCacheKeys.playlistTracks, params.id],
|
[QueryCacheKeys.playlistTracks, params.id],
|
||||||
(spotifyApi) => spotifyApi.getPlaylistTracks(params.id).then((track) => track.body.items),
|
(spotifyApi) =>
|
||||||
{ initialData: [] }
|
spotifyApi.getPlaylistTracks(params.id).then((track) => track.body.items),
|
||||||
|
{ initialData: [] },
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePlaylistPlayPause = () => {
|
const handlePlaylistPlayPause = () => {
|
||||||
@ -38,7 +48,9 @@ const PlaylistView: FC = () => {
|
|||||||
setCurrentPlaylist({ ...params, ...location.state, tracks });
|
setCurrentPlaylist({ ...params, ...location.state, tracks });
|
||||||
setCurrentTrack(tracks[0].track);
|
setCurrentTrack(tracks[0].track);
|
||||||
} else {
|
} else {
|
||||||
audioPlayer.stop().catch((error) => console.error("Failed to stop audio player: ", error));
|
audioPlayer
|
||||||
|
.stop()
|
||||||
|
.catch((error) => console.error("Failed to stop audio player: ", error));
|
||||||
setCurrentTrack(undefined);
|
setCurrentTrack(undefined);
|
||||||
setCurrentPlaylist(undefined);
|
setCurrentPlaylist(undefined);
|
||||||
}
|
}
|
||||||
@ -52,14 +64,28 @@ const PlaylistView: FC = () => {
|
|||||||
isActive={currentPlaylist?.id === params.id}
|
isActive={currentPlaylist?.id === params.id}
|
||||||
isFavorite={isFavorite(params.id)}
|
isFavorite={isFavorite(params.id)}
|
||||||
/>
|
/>
|
||||||
<Text>{`<center><h2>${location.state.name[0].toUpperCase()}${location.state.name.slice(1)}</h2></center>`}</Text>
|
<Text>{`<center><h2>${location.state.name[0].toUpperCase()}${location.state.name.slice(
|
||||||
|
1,
|
||||||
|
)}</h2></center>`}</Text>
|
||||||
{<TrackTableIndex />}
|
{<TrackTableIndex />}
|
||||||
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
<ScrollArea style={`flex:1; flex-grow: 1; border: none;`}>
|
||||||
<View style={`flex-direction:column; flex: 1;`}>
|
<View style={`flex-direction:column; flex: 1;`}>
|
||||||
<PlaceholderApplet error={isError} loading={isLoading} reload={refetch} message={`Failed retrieving ${location.state.name} tracks`} />
|
<PlaceholderApplet
|
||||||
|
error={isError}
|
||||||
|
loading={isLoading}
|
||||||
|
reload={refetch}
|
||||||
|
message={`Failed retrieving ${location.state.name} tracks`}
|
||||||
|
/>
|
||||||
{tracks?.map(({ track }, index) => {
|
{tracks?.map(({ track }, index) => {
|
||||||
if (track) {
|
if (track) {
|
||||||
return <TrackButton key={index + track.id} track={track} index={index} playlist={playlist} />;
|
return (
|
||||||
|
<TrackButton
|
||||||
|
key={index + track.id}
|
||||||
|
track={track}
|
||||||
|
index={index}
|
||||||
|
playlist={playlist}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
@ -88,11 +114,21 @@ interface PlaylistSimpleControlsProps {
|
|||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlaylistSimpleControls({ handlePlaylistPlayPause, isActive, handlePlaylistReact, isFavorite }: PlaylistSimpleControlsProps) {
|
export function PlaylistSimpleControls({
|
||||||
|
handlePlaylistPlayPause,
|
||||||
|
isActive,
|
||||||
|
handlePlaylistReact,
|
||||||
|
isFavorite,
|
||||||
|
}: PlaylistSimpleControlsProps) {
|
||||||
return (
|
return (
|
||||||
<View style={`justify-content: 'space-evenly'; max-width: 150px; padding: 10px;`}>
|
<View style={`justify-content: 'space-evenly'; max-width: 150px; padding: 10px;`}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
{isFavorite !== undefined && <IconButton icon={new QIcon(isFavorite ? heart : heartRegular)} on={{ clicked: handlePlaylistReact }} />}
|
{isFavorite !== undefined && (
|
||||||
|
<IconButton
|
||||||
|
icon={new QIcon(isFavorite ? heart : heartRegular)}
|
||||||
|
on={{ clicked: handlePlaylistReact }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
style={`background-color: #00be5f; color: white;`}
|
style={`background-color: #00be5f; color: white;`}
|
||||||
on={{
|
on={{
|
||||||
|
@ -15,10 +15,18 @@ import { TrackButton } from "./shared/TrackButton";
|
|||||||
function Search() {
|
function Search() {
|
||||||
const history = useHistory<{ searchQuery: string }>();
|
const history = useHistory<{ searchQuery: string }>();
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
const { data: searchResults, refetch, isError, isLoading } = useSpotifyQuery<SpotifyApi.SearchResponse>(
|
const {
|
||||||
|
data: searchResults,
|
||||||
|
refetch,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
} = useSpotifyQuery<SpotifyApi.SearchResponse>(
|
||||||
QueryCacheKeys.search,
|
QueryCacheKeys.search,
|
||||||
(spotifyApi) => spotifyApi.search(searchQuery, ["playlist", "track"], { limit: 4 }).then((res) => res.body),
|
(spotifyApi) =>
|
||||||
{ enabled: false }
|
spotifyApi
|
||||||
|
.search(searchQuery, ["playlist", "track"], { limit: 4 })
|
||||||
|
.then((res) => res.body),
|
||||||
|
{ enabled: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
async function handleSearch() {
|
async function handleSearch() {
|
||||||
@ -29,7 +37,14 @@ function Search() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeholder = <PlaceholderApplet error={isError} loading={isLoading} message="Failed querying spotify" reload={refetch} />;
|
const placeholder = (
|
||||||
|
<PlaceholderApplet
|
||||||
|
error={isError}
|
||||||
|
loading={isLoading}
|
||||||
|
message="Failed querying spotify"
|
||||||
|
reload={refetch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<View style="flex: 1; flex-direction: 'column'; padding: 5px;">
|
<View style="flex: 1; flex-direction: 'column'; padding: 5px;">
|
||||||
<View>
|
<View>
|
||||||
@ -40,16 +55,21 @@ function Search() {
|
|||||||
textChanged(t) {
|
textChanged(t) {
|
||||||
setSearchQuery(t);
|
setSearchQuery(t);
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
KeyRelease(native: any) {
|
KeyRelease(native: any) {
|
||||||
const key = new QKeyEvent(native);
|
const key = new QKeyEvent(native);
|
||||||
const isEnter = key.key() === 16777220;
|
const isEnter = key.key() === 16777220;
|
||||||
if (isEnter) {
|
if (isEnter) {
|
||||||
handleSearch();
|
handleSearch();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton enabled={searchQuery.length > 0} icon={new QIcon(search)} on={{ clicked: handleSearch }} />
|
<IconButton
|
||||||
|
enabled={searchQuery.length > 0}
|
||||||
|
icon={new QIcon(search)}
|
||||||
|
on={{ clicked: handleSearch }}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<ScrollArea style="flex: 1;">
|
<ScrollArea style="flex: 1;">
|
||||||
<View style="flex-direction: 'column'; flex: 1;">
|
<View style="flex-direction: 'column'; flex: 1;">
|
||||||
@ -57,32 +77,51 @@ function Search() {
|
|||||||
<Text
|
<Text
|
||||||
cursor={CursorShape.PointingHandCursor}
|
cursor={CursorShape.PointingHandCursor}
|
||||||
on={{
|
on={{
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
MouseButtonRelease(native: any) {
|
MouseButtonRelease(native: any) {
|
||||||
if (new QMouseEvent(native).button() === 1 && searchResults?.tracks) {
|
if (
|
||||||
|
new QMouseEvent(native).button() === 1 &&
|
||||||
|
searchResults?.tracks
|
||||||
|
) {
|
||||||
history.push("/search/songs", { searchQuery });
|
history.push("/search/songs", { searchQuery });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}}>{`<h2>Songs</h2>`}</Text>
|
}}
|
||||||
|
>{`<h2>Songs</h2>`}</Text>
|
||||||
<TrackTableIndex />
|
<TrackTableIndex />
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{searchResults?.tracks?.items.map((track, index) => (
|
{searchResults?.tracks?.items.map((track, index) => (
|
||||||
<TrackButton key={index + track.id} index={index} track={track} />
|
<TrackButton
|
||||||
|
key={index + track.id}
|
||||||
|
index={index}
|
||||||
|
track={track}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
<View style="flex: 1; flex-direction: 'column';">
|
<View style="flex: 1; flex-direction: 'column';">
|
||||||
<Text
|
<Text
|
||||||
cursor={CursorShape.PointingHandCursor}
|
cursor={CursorShape.PointingHandCursor}
|
||||||
on={{
|
on={{
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
MouseButtonRelease(native: any) {
|
MouseButtonRelease(native: any) {
|
||||||
if (new QMouseEvent(native).button() === 1 && searchResults?.playlists) {
|
if (
|
||||||
history.push("/search/playlists", { searchQuery });
|
new QMouseEvent(native).button() === 1 &&
|
||||||
|
searchResults?.playlists
|
||||||
|
) {
|
||||||
|
history.push("/search/playlists", {
|
||||||
|
searchQuery,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}}>{`<h2>Playlists</h2>`}</Text>
|
}}
|
||||||
|
>{`<h2>Playlists</h2>`}</Text>
|
||||||
<View style="flex: 1; justify-content: 'space-around'; align-items: 'center';">
|
<View style="flex: 1; justify-content: 'space-around'; align-items: 'center';">
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{searchResults?.playlists?.items.map((playlist, index) => (
|
{searchResults?.playlists?.items.map((playlist, index) => (
|
||||||
<PlaylistCard key={index + playlist.id} playlist={playlist} />
|
<PlaylistCard
|
||||||
|
key={index + playlist.id}
|
||||||
|
playlist={playlist}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -6,16 +6,33 @@ import { GenreView } from "./PlaylistGenreView";
|
|||||||
|
|
||||||
function SearchResultPlaylistCollection() {
|
function SearchResultPlaylistCollection() {
|
||||||
const location = useLocation<{ searchQuery: string }>();
|
const location = useLocation<{ searchQuery: string }>();
|
||||||
const { data: searchResults, fetchNextPage, hasNextPage, isFetchingNextPage, isError, isLoading, refetch } = useSpotifyInfiniteQuery<SpotifyApi.SearchResponse>(
|
const {
|
||||||
|
data: searchResults,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
} = useSpotifyInfiniteQuery<SpotifyApi.SearchResponse>(
|
||||||
QueryCacheKeys.searchPlaylist,
|
QueryCacheKeys.searchPlaylist,
|
||||||
(spotifyApi, { pageParam }) => spotifyApi.searchPlaylists(location.state.searchQuery, { limit: 20, offset: pageParam }).then((res) => res.body),
|
(spotifyApi, { pageParam }) =>
|
||||||
|
spotifyApi
|
||||||
|
.searchPlaylists(location.state.searchQuery, {
|
||||||
|
limit: 20,
|
||||||
|
offset: pageParam,
|
||||||
|
})
|
||||||
|
.then((res) => res.body),
|
||||||
{
|
{
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => {
|
||||||
if (lastPage.playlists?.next) {
|
if (lastPage.playlists?.next) {
|
||||||
return (lastPage.playlists?.offset ?? 0) + (lastPage.playlists?.limit ?? 0);
|
return (
|
||||||
|
(lastPage.playlists?.offset ?? 0) +
|
||||||
|
(lastPage.playlists?.limit ?? 0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<GenreView
|
<GenreView
|
||||||
|
@ -9,16 +9,30 @@ import { TrackButton } from "./shared/TrackButton";
|
|||||||
|
|
||||||
function SearchResultSongsCollection() {
|
function SearchResultSongsCollection() {
|
||||||
const location = useLocation<{ searchQuery: string }>();
|
const location = useLocation<{ searchQuery: string }>();
|
||||||
const { data: searchResults, fetchNextPage, hasNextPage, isFetchingNextPage, isError, isLoading, refetch } = useSpotifyInfiniteQuery<SpotifyApi.SearchResponse>(
|
const {
|
||||||
|
data: searchResults,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
} = useSpotifyInfiniteQuery<SpotifyApi.SearchResponse>(
|
||||||
QueryCacheKeys.searchSongs,
|
QueryCacheKeys.searchSongs,
|
||||||
(spotifyApi, { pageParam }) => spotifyApi.searchTracks(location.state.searchQuery, { limit: 20, offset: pageParam }).then((res) => res.body),
|
(spotifyApi, { pageParam }) =>
|
||||||
|
spotifyApi
|
||||||
|
.searchTracks(location.state.searchQuery, {
|
||||||
|
limit: 20,
|
||||||
|
offset: pageParam,
|
||||||
|
})
|
||||||
|
.then((res) => res.body),
|
||||||
{
|
{
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => {
|
||||||
if (lastPage.tracks?.next) {
|
if (lastPage.tracks?.next) {
|
||||||
return (lastPage.tracks?.offset ?? 0) + (lastPage.tracks?.limit ?? 0);
|
return (lastPage.tracks?.offset ?? 0) + (lastPage.tracks?.limit ?? 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<View style="flex: 1; flex-direction: 'column';">
|
<View style="flex: 1; flex-direction: 'column';">
|
||||||
@ -30,12 +44,26 @@ function SearchResultSongsCollection() {
|
|||||||
<TrackTableIndex />
|
<TrackTableIndex />
|
||||||
<ScrollArea style="flex: 1; border: none;">
|
<ScrollArea style="flex: 1; border: none;">
|
||||||
<View style="flex: 1; flex-direction: 'column';">
|
<View style="flex: 1; flex-direction: 'column';">
|
||||||
<PlaceholderApplet error={isError} loading={isLoading || isFetchingNextPage} message="Failed querying spotify" reload={refetch} />
|
<PlaceholderApplet
|
||||||
|
error={isError}
|
||||||
|
loading={isLoading || isFetchingNextPage}
|
||||||
|
message="Failed querying spotify"
|
||||||
|
reload={refetch}
|
||||||
|
/>
|
||||||
{searchResults?.pages
|
{searchResults?.pages
|
||||||
.map((searchResult) => searchResult.tracks?.items)
|
.map((searchResult) => searchResult.tracks?.items)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.flat(1)
|
.flat(1)
|
||||||
.map((track, index) => track && <TrackButton key={index + track.id} index={index} track={track} />)}
|
.map(
|
||||||
|
(track, index) =>
|
||||||
|
track && (
|
||||||
|
<TrackButton
|
||||||
|
key={index + track.id}
|
||||||
|
index={index}
|
||||||
|
track={track}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
|
||||||
{hasNextPage && (
|
{hasNextPage && (
|
||||||
<Button
|
<Button
|
||||||
|
@ -4,7 +4,7 @@ import preferencesContext from "../context/preferencesContext";
|
|||||||
import Switch, { SwitchProps } from "./shared/Switch";
|
import Switch, { SwitchProps } from "./shared/Switch";
|
||||||
|
|
||||||
function Settings() {
|
function Settings() {
|
||||||
const { setPreferences, ...preferences } = useContext(preferencesContext)
|
const { setPreferences, ...preferences } = useContext(preferencesContext);
|
||||||
return (
|
return (
|
||||||
<View style="flex: 1; flex-direction: 'column'; justify-content: 'flex-start';">
|
<View style="flex: 1; flex-direction: 'column'; justify-content: 'flex-start';">
|
||||||
<Text>{`<center><h2>Settings</h2></center>`}</Text>
|
<Text>{`<center><h2>Settings</h2></center>`}</Text>
|
||||||
@ -13,7 +13,9 @@ function Settings() {
|
|||||||
checked={preferences.playlistImages}
|
checked={preferences.playlistImages}
|
||||||
title="Use images instead of colors for playlist"
|
title="Use images instead of colors for playlist"
|
||||||
subtitle="This will increase memory usage"
|
subtitle="This will increase memory usage"
|
||||||
onChange={(checked) => setPreferences({ ...preferences, playlistImages: checked })}
|
onChange={(checked) =>
|
||||||
|
setPreferences({ ...preferences, playlistImages: checked })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -29,7 +31,12 @@ interface SettingsCheckTileProps {
|
|||||||
onChange?: SwitchProps["onChange"];
|
onChange?: SwitchProps["onChange"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsCheckTile({ title, subtitle = "", onChange, checked }: SettingsCheckTileProps) {
|
export function SettingsCheckTile({
|
||||||
|
title,
|
||||||
|
subtitle = "",
|
||||||
|
onChange,
|
||||||
|
checked,
|
||||||
|
}: SettingsCheckTileProps) {
|
||||||
return (
|
return (
|
||||||
<View style="flex: 1; align-items: 'center'; padding: 15px 0; justify-content: 'space-between';">
|
<View style="flex: 1; align-items: 'center'; padding: 15px 0; justify-content: 'space-between';">
|
||||||
<Text>
|
<Text>
|
||||||
|
@ -55,7 +55,7 @@ export interface TabMenuItemProps {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TabMenuItem({ icon, title, url }: TabMenuItemProps) {
|
export function TabMenuItem({ title, url }: TabMenuItemProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@ -63,5 +63,15 @@ export function TabMenuItem({ icon, title, url }: TabMenuItemProps) {
|
|||||||
history.push(url);
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -34,15 +34,22 @@ function CachedImage({ src, alt, size, maxSize, ...props }: CachedImageProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (imageBuffer) {
|
if (imageBuffer) {
|
||||||
pixmap.loadFromData(imageBuffer);
|
pixmap.loadFromData(imageBuffer);
|
||||||
pixmap.scaled((size ?? maxSize)?.height ?? 100, (size ?? maxSize)?.width ?? 100);
|
pixmap.scaled(
|
||||||
|
(size ?? maxSize)?.height ?? 100,
|
||||||
|
(size ?? maxSize)?.width ?? 100,
|
||||||
|
);
|
||||||
labelRef.current?.setPixmap(pixmap);
|
labelRef.current?.setPixmap(pixmap);
|
||||||
}
|
}
|
||||||
}, [imageBuffer]);
|
}, [imageBuffer]);
|
||||||
|
|
||||||
return !imageProcessError && imageBuffer ? (
|
return !imageProcessError && imageBuffer ? (
|
||||||
<Text ref={labelRef} {...props}/>
|
<Text ref={labelRef} {...props} />
|
||||||
) : alt ? (
|
) : alt ? (
|
||||||
<View style={`padding: ${((maxSize ?? size)?.height || 10) / 2.5}px ${((maxSize ?? size)?.width || 10) / 2.5}px;`}>
|
<View
|
||||||
|
style={`padding: ${((maxSize ?? size)?.height || 10) / 2.5}px ${
|
||||||
|
((maxSize ?? size)?.width || 10) / 2.5
|
||||||
|
}px;`}
|
||||||
|
>
|
||||||
<Text>{alt}</Text>
|
<Text>{alt}</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
|
@ -37,6 +37,7 @@ const categoryStylesheet = `
|
|||||||
`;
|
`;
|
||||||
const CategoryCard: FC<CategoryCardProps> = ({ name, isError, playlists, url }) => {
|
const CategoryCard: FC<CategoryCardProps> = ({ name, isError, playlists, url }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function goToGenre(native: any) {
|
function goToGenre(native: any) {
|
||||||
const mouse = new QMouseEvent(native);
|
const mouse = new QMouseEvent(native);
|
||||||
if (mouse.button() === 1) {
|
if (mouse.button() === 1) {
|
||||||
@ -48,7 +49,12 @@ const CategoryCard: FC<CategoryCardProps> = ({ name, isError, playlists, url })
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<View id="container" styleSheet={categoryStylesheet}>
|
<View id="container" styleSheet={categoryStylesheet}>
|
||||||
<Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />
|
<Button
|
||||||
|
id="anchor-heading"
|
||||||
|
cursor={CursorShape.PointingHandCursor}
|
||||||
|
on={{ MouseButtonRelease: goToGenre }}
|
||||||
|
text={name}
|
||||||
|
/>
|
||||||
<View id="child-view">
|
<View id="child-view">
|
||||||
{playlists.map((playlist, index) => {
|
{playlists.map((playlist, index) => {
|
||||||
return <PlaylistCard key={index + playlist.id} playlist={playlist} />;
|
return <PlaylistCard key={index + playlist.id} playlist={playlist} />;
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
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 { Button } from "@nodegui/react-nodegui";
|
||||||
import { ButtonProps } from "@nodegui/react-nodegui/dist/components/Button/RNButton";
|
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) {
|
function IconButton({ style, ...props }: IconButtonProps) {
|
||||||
const iconBtnRef = useRef<QPushButton>()
|
const iconBtnRef = useRef<QPushButton>();
|
||||||
const shadowGfx = new QGraphicsDropShadowEffect();
|
const shadowGfx = new QGraphicsDropShadowEffect();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
shadowGfx.setBlurRadius(5);
|
shadowGfx.setBlurRadius(5);
|
||||||
shadowGfx.setXOffset(0);
|
shadowGfx.setXOffset(0);
|
||||||
shadowGfx.setYOffset(0);
|
shadowGfx.setYOffset(0);
|
||||||
iconBtnRef.current?.setGraphicsEffect(shadowGfx);
|
iconBtnRef.current?.setGraphicsEffect(shadowGfx);
|
||||||
}, [])
|
}, []);
|
||||||
const iconButtonStyleSheet = `
|
const iconButtonStyleSheet = `
|
||||||
#icon-btn{
|
#icon-btn{
|
||||||
background-color: rgba(255, 255, 255, 0.055);
|
background-color: rgba(255, 255, 255, 0.055);
|
||||||
@ -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;
|
export default IconButton;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { View, Button, Text } from "@nodegui/react-nodegui";
|
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 React, { useEffect, useRef } from "react";
|
||||||
import { loadingSpinner } from "../../icons";
|
import { loadingSpinner } from "../../icons";
|
||||||
|
|
||||||
@ -7,7 +7,7 @@ interface ErrorAppletProps {
|
|||||||
error: boolean;
|
error: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
reload: Function;
|
reload: () => void;
|
||||||
helps?: boolean;
|
helps?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,14 +21,20 @@ interface PlaylistCardProps {
|
|||||||
const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
|
const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
|
||||||
const preferences = useContext(preferencesContext);
|
const preferences = useContext(preferencesContext);
|
||||||
const thumbnail = playlist.images[0].url;
|
const thumbnail = playlist.images[0].url;
|
||||||
const { id, description, name, images } = playlist;
|
const { id, description, name } = playlist;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const { setCurrentTrack, currentPlaylist, setCurrentPlaylist } = useContext(playerContext);
|
const { setCurrentTrack, currentPlaylist, setCurrentPlaylist } =
|
||||||
const { refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>([QueryCacheKeys.playlistTracks, id], (spotifyApi) => spotifyApi.getPlaylistTracks(id).then((track) => track.body.items), {
|
useContext(playerContext);
|
||||||
|
const { refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>(
|
||||||
|
[QueryCacheKeys.playlistTracks, id],
|
||||||
|
(spotifyApi) =>
|
||||||
|
spotifyApi.getPlaylistTracks(id).then((track) => track.body.items),
|
||||||
|
{
|
||||||
initialData: [],
|
initialData: [],
|
||||||
enabled: false,
|
enabled: false,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
const { reactToPlaylist, isFavorite } = usePlaylistReaction();
|
const { reactToPlaylist, isFavorite } = usePlaylistReaction();
|
||||||
|
|
||||||
const handlePlaylistPlayPause = async () => {
|
const handlePlaylistPlayPause = async () => {
|
||||||
@ -47,6 +53,7 @@ const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function gotoPlaylist(native?: any) {
|
function gotoPlaylist(native?: any) {
|
||||||
const key = new QMouseEvent(native);
|
const key = new QMouseEvent(native);
|
||||||
if (key.button() === 1) {
|
if (key.button() === 1) {
|
||||||
@ -121,10 +128,22 @@ const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
|
|||||||
on={{
|
on={{
|
||||||
MouseButtonRelease: gotoPlaylist,
|
MouseButtonRelease: gotoPlaylist,
|
||||||
...hovers,
|
...hovers,
|
||||||
}}>
|
}}
|
||||||
{preferences.playlistImages && <CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} />}
|
>
|
||||||
|
{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>
|
<center>
|
||||||
<h3>${name}</h3>
|
<h3>${name}</h3>
|
||||||
@ -133,11 +152,14 @@ const PlaylistCard = ({ playlist }: PlaylistCardProps) => {
|
|||||||
`}
|
`}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{(hovered || currentPlaylist?.id === id) && !preferences.playlistImages && playlistActions}
|
{(hovered || currentPlaylist?.id === id) &&
|
||||||
{preferences.playlistImages &&
|
!preferences.playlistImages &&
|
||||||
<View style="flex: 1; justify-content: 'space-around';">{playlistActions}
|
playlistActions}
|
||||||
|
{preferences.playlistImages && (
|
||||||
|
<View style="flex: 1; justify-content: 'space-around';">
|
||||||
|
{playlistActions}
|
||||||
</View>
|
</View>
|
||||||
}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -27,8 +27,9 @@ function Switch({ checked: derivedChecked, onChange, ...props }: SwitchProps) {
|
|||||||
maxSize={{ width: 30, height: 20 }}
|
maxSize={{ width: 30, height: 20 }}
|
||||||
on={{
|
on={{
|
||||||
valueChanged(value) {
|
valueChanged(value) {
|
||||||
onChange && onChange(value===1);
|
onChange && onChange(value === 1);
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
MouseButtonRelease(native: any) {
|
MouseButtonRelease(native: any) {
|
||||||
const mouse = new QMouseEvent(native);
|
const mouse = new QMouseEvent(native);
|
||||||
if (mouse.button() === 1) {
|
if (mouse.button() === 1) {
|
||||||
|
@ -9,7 +9,9 @@ import IconButton from "./IconButton";
|
|||||||
|
|
||||||
export interface TrackButtonPlaylistObject extends SpotifyApi.PlaylistBaseObject {
|
export interface TrackButtonPlaylistObject extends SpotifyApi.PlaylistBaseObject {
|
||||||
follower?: SpotifyApi.FollowersObject;
|
follower?: SpotifyApi.FollowersObject;
|
||||||
tracks: SpotifyApi.PagingObject<SpotifyApi.SavedTrackObject | SpotifyApi.PlaylistTrackObject>;
|
tracks: SpotifyApi.PagingObject<
|
||||||
|
SpotifyApi.SavedTrackObject | SpotifyApi.PlaylistTrackObject
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrackButtonProps {
|
export interface TrackButtonProps {
|
||||||
@ -20,10 +22,16 @@ export interface TrackButtonProps {
|
|||||||
|
|
||||||
export const TrackButton: FC<TrackButtonProps> = ({ track, index, playlist }) => {
|
export const TrackButton: FC<TrackButtonProps> = ({ track, index, playlist }) => {
|
||||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||||
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack, currentTrack } = useContext(playerContext);
|
const { currentPlaylist, setCurrentPlaylist, setCurrentTrack, currentTrack } =
|
||||||
|
useContext(playerContext);
|
||||||
const handlePlaylistPlayPause = (index: number) => {
|
const handlePlaylistPlayPause = (index: number) => {
|
||||||
if (playlist && currentPlaylist?.id !== playlist.id) {
|
if (playlist && currentPlaylist?.id !== playlist.id) {
|
||||||
const globalPlaylistObj = { id: playlist.id, name: playlist.name, thumbnail: playlist.images[0].url, tracks: playlist.tracks.items };
|
const globalPlaylistObj = {
|
||||||
|
id: playlist.id,
|
||||||
|
name: playlist.name,
|
||||||
|
thumbnail: playlist.images[0].url,
|
||||||
|
tracks: playlist.tracks.items,
|
||||||
|
};
|
||||||
setCurrentPlaylist(globalPlaylistObj);
|
setCurrentPlaylist(globalPlaylistObj);
|
||||||
setCurrentTrack(playlist.tracks.items[index].track);
|
setCurrentTrack(playlist.tracks.items[index].track);
|
||||||
}
|
}
|
||||||
@ -34,18 +42,22 @@ export const TrackButton: FC<TrackButtonProps> = ({ track, index, playlist }) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const duration = useMemo(() => msToMinAndSec(track.duration_ms), []);
|
const duration = useMemo(() => msToMinAndSec(track.duration_ms), []);
|
||||||
const active = (currentPlaylist?.id === playlist?.id && currentTrack?.id === track.id) || currentTrack?.id === track.id;
|
const active =
|
||||||
|
(currentPlaylist?.id === playlist?.id && currentTrack?.id === track.id) ||
|
||||||
|
currentTrack?.id === track.id;
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
id={active ? "active" : "track-button"}
|
id={active ? "active" : "track-button"}
|
||||||
styleSheet={trackButtonStyle}
|
styleSheet={trackButtonStyle}
|
||||||
on={{
|
on={{
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
MouseButtonRelease(native: any) {
|
MouseButtonRelease(native: any) {
|
||||||
if (new QMouseEvent(native).button() === 1 && playlist) {
|
if (new QMouseEvent(native).button() === 1 && playlist) {
|
||||||
handlePlaylistPlayPause(index);
|
handlePlaylistPlayPause(index);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Text style="padding: 5px;">{index + 1}</Text>
|
<Text style="padding: 5px;">{index + 1}</Text>
|
||||||
<View style="flex-direction: 'column'; width: '35%';">
|
<View style="flex-direction: 'column'; width: '35%';">
|
||||||
<Text>{`<h3>${track.name}</h3>`}</Text>
|
<Text>{`<h3>${track.name}</h3>`}</Text>
|
||||||
|
@ -2,7 +2,7 @@ import dotenv from "dotenv";
|
|||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
import { join } from "path";
|
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 clientId = "";
|
||||||
export const trace = process.argv.find((arg) => arg === "--trace") ?? false;
|
export const trace = process.argv.find((arg) => arg === "--trace") ?? false;
|
||||||
export const redirectURI = "http://localhost:4304/auth/spotify/callback";
|
export const redirectURI = "http://localhost:4304/auth/spotify/callback";
|
||||||
@ -20,6 +20,7 @@ export enum QueryCacheKeys {
|
|||||||
search = "search",
|
search = "search",
|
||||||
searchPlaylist = "searchPlaylist",
|
searchPlaylist = "searchPlaylist",
|
||||||
searchSongs = "searchSongs",
|
searchSongs = "searchSongs",
|
||||||
|
followedArtists = "followed-artists",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LocalStorageKeys {
|
export enum LocalStorageKeys {
|
||||||
@ -28,5 +29,5 @@ export enum LocalStorageKeys {
|
|||||||
preferences = "user-preferences",
|
preferences = "user-preferences",
|
||||||
volume = "volume",
|
volume = "volume",
|
||||||
cachedPlaylist = "cached-playlist",
|
cachedPlaylist = "cached-playlist",
|
||||||
cachedTrack = "cached-track"
|
cachedTrack = "cached-track",
|
||||||
}
|
}
|
||||||
|
@ -7,18 +7,24 @@ export interface AuthContext {
|
|||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
access_token: string;
|
access_token: string;
|
||||||
setCredentials: Dispatch<SetStateAction<Credentials>>
|
setCredentials: Dispatch<SetStateAction<Credentials>>;
|
||||||
setAccess_token: Dispatch<SetStateAction<string>>;
|
setAccess_token: Dispatch<SetStateAction<string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authContext = React.createContext<AuthContext>({
|
const authContext = React.createContext<AuthContext>({
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
setIsLoggedIn() {},
|
setIsLoggedIn() {
|
||||||
|
return;
|
||||||
|
},
|
||||||
access_token: "",
|
access_token: "",
|
||||||
clientId: "",
|
clientId: "",
|
||||||
clientSecret: "",
|
clientSecret: "",
|
||||||
setCredentials(){},
|
setCredentials() {
|
||||||
setAccess_token() {},
|
return;
|
||||||
|
},
|
||||||
|
setAccess_token() {
|
||||||
|
return;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default authContext;
|
export default authContext;
|
||||||
|
@ -2,7 +2,12 @@ import React, { Dispatch, SetStateAction } from "react";
|
|||||||
|
|
||||||
export type CurrentTrack = SpotifyApi.TrackObjectFull;
|
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 {
|
export interface PlayerContext {
|
||||||
currentPlaylist?: CurrentPlaylist;
|
currentPlaylist?: CurrentPlaylist;
|
||||||
@ -11,6 +16,13 @@ export interface PlayerContext {
|
|||||||
setCurrentTrack: Dispatch<SetStateAction<CurrentTrack | 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;
|
export default playerContext;
|
||||||
|
@ -9,7 +9,9 @@ export interface PreferencesContext extends PreferencesContextProperties {
|
|||||||
|
|
||||||
const preferencesContext = React.createContext<PreferencesContext>({
|
const preferencesContext = React.createContext<PreferencesContext>({
|
||||||
playlistImages: false,
|
playlistImages: false,
|
||||||
setPreferences() { }
|
setPreferences() {
|
||||||
|
return;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default preferencesContext;
|
export default preferencesContext;
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import color from "color";
|
import color from "color";
|
||||||
|
|
||||||
export function generateRandomColor(lightness: number=70): string {
|
export function generateRandomColor(lightness = 70): string {
|
||||||
return "hsl(" + 360 * Math.random() + "," + (25 + 70 * Math.random()) + "%," + (lightness + 10 * Math.random()) + "%)";
|
return (
|
||||||
|
"hsl(" +
|
||||||
|
360 * Math.random() +
|
||||||
|
"," +
|
||||||
|
(25 + 70 * Math.random()) +
|
||||||
|
"%," +
|
||||||
|
(lightness + 10 * Math.random()) +
|
||||||
|
"%)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDarkenForeground(hslcolor: string): string {
|
export function getDarkenForeground(hslcolor: string): string {
|
||||||
const adjustedColor = color(hslcolor);
|
const adjustedColor = color(hslcolor);
|
||||||
return adjustedColor.darken(.5).hex();
|
return adjustedColor.darken(0.5).hex();
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,67 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import htmlToText from "html-to-text";
|
import htmlToText from "html-to-text";
|
||||||
import showError from "./showError";
|
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 delim1 =
|
||||||
const delim2 = '</div></div></div></div></div><div><span class="hwc"><div class="BNeawe uEec3 AP7Wnd">';
|
'</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=";
|
const url = "https://www.google.com/search?q=";
|
||||||
|
|
||||||
export default async function fetchLyrics(artists: string, title: string) {
|
export default async function fetchLyrics(artists: string, title: string) {
|
||||||
let lyrics;
|
let lyrics;
|
||||||
try {
|
try {
|
||||||
console.log("[lyric query]:", `${url}${encodeURIComponent(title + " " + artists)}+lyrics`);
|
console.log(
|
||||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}+lyrics`, { responseType: "text" })).data;
|
"[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(delim1);
|
||||||
[lyrics] = lyrics.split(delim2);
|
[lyrics] = lyrics.split(delim2);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err, "[Lyric Query Error]: ");
|
showError(err, "[Lyric Query Error]: ");
|
||||||
try {
|
try {
|
||||||
console.log("[lyric query]:", `${url}${encodeURIComponent(title + " " + artists)}+song+lyrics`);
|
console.log(
|
||||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}+song+lyrics`)).data;
|
"[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(delim1);
|
||||||
[lyrics] = lyrics.split(delim2);
|
[lyrics] = lyrics.split(delim2);
|
||||||
} catch (err_1) {
|
} catch (err_1) {
|
||||||
showError(err_1, "[Lyric Query Error]: ");
|
showError(err_1, "[Lyric Query Error]: ");
|
||||||
try {
|
try {
|
||||||
console.log("[lyric query]:", `${url}${encodeURIComponent(title + " " + artists)}+song`);
|
console.log(
|
||||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}+song`)).data;
|
"[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(delim1);
|
||||||
[lyrics] = lyrics.split(delim2);
|
[lyrics] = lyrics.split(delim2);
|
||||||
} catch (err_2) {
|
} catch (err_2) {
|
||||||
showError(err_2, "[Lyric Query Error]: ");
|
showError(err_2, "[Lyric Query Error]: ");
|
||||||
try {
|
try {
|
||||||
console.log("[lyric query]:", `${url}${encodeURIComponent(title + " " + artists)}`);
|
console.log(
|
||||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}`)).data;
|
"[lyric query]:",
|
||||||
|
`${url}${encodeURIComponent(title + " " + artists)}`,
|
||||||
|
);
|
||||||
|
lyrics = (
|
||||||
|
await axios.get<string>(
|
||||||
|
`${url}${encodeURIComponent(title + " " + artists)}`,
|
||||||
|
)
|
||||||
|
).data;
|
||||||
[, lyrics] = lyrics.split(delim1);
|
[, lyrics] = lyrics.split(delim1);
|
||||||
[lyrics] = lyrics.split(delim2);
|
[lyrics] = lyrics.split(delim2);
|
||||||
} catch (err_3) {
|
} catch (err_3) {
|
||||||
|
@ -15,7 +15,10 @@ interface ImageDimensions {
|
|||||||
|
|
||||||
const fsm = fs.promises;
|
const fsm = fs.promises;
|
||||||
|
|
||||||
export async function getCachedImageBuffer(name: string, dims?: ImageDimensions): Promise<Buffer> {
|
export async function getCachedImageBuffer(
|
||||||
|
name: string,
|
||||||
|
dims?: ImageDimensions,
|
||||||
|
): Promise<Buffer> {
|
||||||
try {
|
try {
|
||||||
const MB_5 = 5000000; //5 Megabytes
|
const MB_5 = 5000000; //5 Megabytes
|
||||||
const cacheImgFolder = path.join(cacheDir, "images");
|
const cacheImgFolder = path.join(cacheDir, "images");
|
||||||
@ -33,20 +36,34 @@ export async function getCachedImageBuffer(name: string, dims?: ImageDimensions)
|
|||||||
|
|
||||||
// if the dimensions are changed then the previously cached
|
// if the dimensions are changed then the previously cached
|
||||||
// images are removed and replaced with a new one
|
// images are removed and replaced with a new one
|
||||||
if (dims && (cachedImgMeta.height !== dims.height || cachedImgMeta.width !== dims?.width)) {
|
if (
|
||||||
|
dims &&
|
||||||
|
(cachedImgMeta.height !== dims.height ||
|
||||||
|
cachedImgMeta.width !== dims?.width)
|
||||||
|
) {
|
||||||
fs.unlinkSync(cacheImgPath);
|
fs.unlinkSync(cacheImgPath);
|
||||||
return await imageResizeAndWrite(cachedImg, { cacheFolder: cacheImgFolder, cacheName, dims });
|
return await imageResizeAndWrite(cachedImg, {
|
||||||
|
cacheFolder: cacheImgFolder,
|
||||||
|
cacheName,
|
||||||
|
dims,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return cachedImg;
|
return cachedImg;
|
||||||
} else {
|
} else {
|
||||||
// finding no cache image fetching it through axios
|
// finding no cache image fetching it through axios
|
||||||
const { data: imgData } = await axios.get<Stream>(name, { responseType: "stream" });
|
const { data: imgData } = await axios.get<Stream>(name, {
|
||||||
|
responseType: "stream",
|
||||||
|
});
|
||||||
// converting axios stream to buffer
|
// converting axios stream to buffer
|
||||||
const resImgBuf = await streamToBuffer(imgData);
|
const resImgBuf = await streamToBuffer(imgData);
|
||||||
// creating cache_dir
|
// creating cache_dir
|
||||||
await fsm.mkdir(cacheImgFolder, { recursive: true });
|
await fsm.mkdir(cacheImgFolder, { recursive: true });
|
||||||
if (dims) {
|
if (dims) {
|
||||||
return await imageResizeAndWrite(resImgBuf, { cacheFolder: cacheImgFolder, cacheName, dims });
|
return await imageResizeAndWrite(resImgBuf, {
|
||||||
|
cacheFolder: cacheImgFolder,
|
||||||
|
cacheName,
|
||||||
|
dims,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await fsm.writeFile(path.join(cacheImgFolder, cacheName), resImgBuf);
|
await fsm.writeFile(path.join(cacheImgFolder, cacheName), resImgBuf);
|
||||||
return resImgBuf;
|
return resImgBuf;
|
||||||
@ -57,7 +74,14 @@ export async function getCachedImageBuffer(name: string, dims?: ImageDimensions)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function imageResizeAndWrite(img: Buffer, { cacheFolder, cacheName, dims }: { dims: ImageDimensions; cacheFolder: string; cacheName: string }): Promise<Buffer> {
|
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)
|
// caching the images by resizing if the max/fixed (Width/Height)
|
||||||
// is available in the args
|
// is available in the args
|
||||||
const resizedImg = (await Jimp.read(img)).resize(dims.width, dims.height);
|
const resizedImg = (await Jimp.read(img)).resize(dims.width, dims.height);
|
||||||
|
@ -10,7 +10,10 @@ import { CurrentTrack } from "../context/playerContext";
|
|||||||
* @param {(Array<string | number>)} matches
|
* @param {(Array<string | number>)} matches
|
||||||
* @return {*} {number}
|
* @return {*} {number}
|
||||||
*/
|
*/
|
||||||
export function includePercentage(src: string | Array<string | number>, matches: Array<string | number>): number {
|
export function includePercentage(
|
||||||
|
src: string | Array<string | number>,
|
||||||
|
matches: Array<string | number>,
|
||||||
|
): number {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
matches.forEach((match) => {
|
matches.forEach((match) => {
|
||||||
if (src.includes(match.toString())) count++;
|
if (src.includes(match.toString())) count++;
|
||||||
@ -22,27 +25,51 @@ export interface YoutubeTrack extends CurrentTrack {
|
|||||||
youtube_uri: string;
|
youtube_uri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getYoutubeTrack(track: SpotifyApi.TrackObjectFull): Promise<YoutubeTrack> {
|
export async function getYoutubeTrack(
|
||||||
|
track: SpotifyApi.TrackObjectFull,
|
||||||
|
): Promise<YoutubeTrack> {
|
||||||
try {
|
try {
|
||||||
const artistsName = track.artists.map((ar) => ar.name);
|
const artistsName = track.artists.map((ar) => ar.name);
|
||||||
const queryString = `${artistsName[0]} - ${track.name}${artistsName.length > 1 ? ` feat. ${artistsName.slice(1).join(" ")}` : ``}`;
|
const queryString = `${artistsName[0]} - ${track.name}${
|
||||||
|
artistsName.length > 1 ? ` feat. ${artistsName.slice(1).join(" ")}` : ``
|
||||||
|
}`;
|
||||||
console.log("Youtube Query String:", queryString);
|
console.log("Youtube Query String:", queryString);
|
||||||
const result = await scrapYt.search(queryString, { limit: 7, type: "video" });
|
const result = await scrapYt.search(queryString, { limit: 7, type: "video" });
|
||||||
const tracksWithRelevance = result
|
const tracksWithRelevance = result
|
||||||
.map((video) => {
|
.map((video) => {
|
||||||
// percentage of matched track {name, artists} matched with
|
// percentage of matched track {name, artists} matched with
|
||||||
// title of the youtube search results
|
// title of the youtube search results
|
||||||
const matchPercentage = includePercentage(video.title, [track.name, ...artistsName]);
|
const matchPercentage = includePercentage(video.title, [
|
||||||
|
track.name,
|
||||||
|
...artistsName,
|
||||||
|
]);
|
||||||
// keeps only those tracks which are from the same artist channel
|
// 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);
|
const sameChannel =
|
||||||
return { url: `http://www.youtube.com/watch?v=${video.id}`, matchPercentage, sameChannel, id: track.id };
|
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));
|
.sort((a, b) => (a.matchPercentage > b.matchPercentage ? -1 : 1));
|
||||||
const sameChannelTracks = tracksWithRelevance.filter((tr) => tr.sameChannel);
|
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 };
|
const finalTrack = {
|
||||||
|
...track,
|
||||||
|
youtube_uri: (sameChannelTracks.length > 0
|
||||||
|
? sameChannelTracks
|
||||||
|
: tracksWithRelevance.length > 0
|
||||||
|
? tracksWithRelevance
|
||||||
|
: rarestTrack)[0].url,
|
||||||
|
};
|
||||||
return finalTrack;
|
return finalTrack;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to resolve track's youtube url: ", error);
|
console.error("Failed to resolve track's youtube url: ", error);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export function msToMinAndSec(ms: number) {
|
export function msToMinAndSec(ms: number) {
|
||||||
const minutes = Math.floor(ms / 60000);
|
const minutes = Math.floor(ms / 60000);
|
||||||
const seconds:number = parseInt(((ms % 60000) / 1000).toFixed(0));
|
const seconds: number = parseInt(((ms % 60000) / 1000).toFixed(0));
|
||||||
return minutes + ":" + (seconds < 10 ? '0' : '') + seconds;
|
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { trace } from "../conf";
|
import { trace } from "../conf";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
||||||
function showError(error: any, message: any="[Error]: ") {
|
function showError(error: Error | TypeError, message = "[Error]: ") {
|
||||||
console.error(chalk.red(message), trace ? error : error.message);
|
console.error(chalk.red(message), trace ? error : error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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--) {
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
const temp = array[i];
|
const temp = array[i];
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Stream } from "stream";
|
import { Stream } from "stream";
|
||||||
|
|
||||||
export function streamToBuffer(stream: Stream): Promise<Buffer> {
|
export function streamToBuffer(stream: Stream): Promise<Buffer> {
|
||||||
let buffArr: any[] = [];
|
const buffArr: Uint8Array[] = [];
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
stream.on("data", (data) => {
|
stream.on("data", (data) => {
|
||||||
buffArr.push(data);
|
buffArr.push(data);
|
||||||
|
@ -40,9 +40,9 @@ function useDownloadQueue() {
|
|||||||
`${el.name} - ${el.artists
|
`${el.name} - ${el.artists
|
||||||
.map((x) => x.name)
|
.map((x) => x.name)
|
||||||
.join(", ")
|
.join(", ")
|
||||||
.trim()}.mp3`
|
.trim()}.mp3`,
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
.on("error", (err) => {
|
.on("error", (err) => {
|
||||||
showError(err, `[failed to download ${el.name}]: `);
|
showError(err, `[failed to download ${el.name}]: `);
|
||||||
|
@ -5,17 +5,25 @@ import useSpotifyMutation from "./useSpotifyMutation";
|
|||||||
|
|
||||||
function usePlaylistReaction() {
|
function usePlaylistReaction() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: favoritePagedPlaylists } = useSpotifyInfiniteQuery<SpotifyApi.ListOfUsersPlaylistsResponse>(QueryCacheKeys.userPlaylists, (spotifyApi, { pageParam }) =>
|
const { data: favoritePagedPlaylists } =
|
||||||
spotifyApi.getUserPlaylists({ limit: 20, offset: pageParam }).then((userPlaylists) => {
|
useSpotifyInfiniteQuery<SpotifyApi.ListOfUsersPlaylistsResponse>(
|
||||||
|
QueryCacheKeys.userPlaylists,
|
||||||
|
(spotifyApi, { pageParam }) =>
|
||||||
|
spotifyApi
|
||||||
|
.getUserPlaylists({ limit: 20, offset: pageParam })
|
||||||
|
.then((userPlaylists) => {
|
||||||
return userPlaylists.body;
|
return userPlaylists.body;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
const favoritePlaylists = favoritePagedPlaylists?.pages
|
const favoritePlaylists = favoritePagedPlaylists?.pages
|
||||||
.map((playlist) => playlist.items)
|
.map((playlist) => playlist.items)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.flat(1) as SpotifyApi.PlaylistObjectSimplified[];
|
.flat(1) as SpotifyApi.PlaylistObjectSimplified[];
|
||||||
|
|
||||||
function updateFunction(playlist: SpotifyApi.PlaylistObjectSimplified, old?: InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse>): InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse> {
|
function updateFunction(
|
||||||
|
playlist: SpotifyApi.PlaylistObjectSimplified,
|
||||||
|
old?: InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse>,
|
||||||
|
): InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse> {
|
||||||
const obj: typeof old = {
|
const obj: typeof old = {
|
||||||
pageParams: old?.pageParams ?? [],
|
pageParams: old?.pageParams ?? [],
|
||||||
pages:
|
pages:
|
||||||
@ -25,22 +33,35 @@ function usePlaylistReaction() {
|
|||||||
if (index === 0 && !isPlaylistFavorite) {
|
if (index === 0 && !isPlaylistFavorite) {
|
||||||
return { ...oldPage, items: [...oldPage.items, playlist] };
|
return { ...oldPage, items: [...oldPage.items, playlist] };
|
||||||
} else if (isPlaylistFavorite) {
|
} else if (isPlaylistFavorite) {
|
||||||
return { ...oldPage, items: oldPage.items.filter((oldPlaylist) => oldPlaylist.id !== playlist.id) };
|
return {
|
||||||
|
...oldPage,
|
||||||
|
items: oldPage.items.filter(
|
||||||
|
(oldPlaylist) => oldPlaylist.id !== playlist.id,
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return oldPage;
|
return oldPage;
|
||||||
}
|
},
|
||||||
) ?? [],
|
) ?? [],
|
||||||
};
|
};
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { mutate: reactToPlaylist } = useSpotifyMutation<{}, SpotifyApi.PlaylistObjectSimplified>(
|
const { mutate: reactToPlaylist } = useSpotifyMutation<
|
||||||
(spotifyApi, { id }) => spotifyApi[isFavorite(id) ? "unfollowPlaylist" : "followPlaylist"](id).then((res) => res.body),
|
unknown,
|
||||||
|
SpotifyApi.PlaylistObjectSimplified
|
||||||
|
>(
|
||||||
|
(spotifyApi, { id }) =>
|
||||||
|
spotifyApi[isFavorite(id) ? "unfollowPlaylist" : "followPlaylist"](id).then(
|
||||||
|
(res) => res.body,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
onSuccess(_, playlist) {
|
onSuccess(_, playlist) {
|
||||||
queryClient.setQueryData<InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse>>(QueryCacheKeys.userPlaylists, (old) => updateFunction(playlist, old));
|
queryClient.setQueryData<
|
||||||
|
InfiniteData<SpotifyApi.ListOfUsersPlaylistsResponse>
|
||||||
|
>(QueryCacheKeys.userPlaylists, (old) => updateFunction(playlist, old));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
);
|
);
|
||||||
const favoritePlaylistIds = favoritePlaylists?.map((playlist) => playlist.id);
|
const favoritePlaylistIds = favoritePlaylists?.map((playlist) => playlist.id);
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import chalk from "chalk";
|
|
||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import { LocalStorageKeys } from "../conf";
|
import { LocalStorageKeys } from "../conf";
|
||||||
import authContext from "../context/authContext";
|
import authContext from "../context/authContext";
|
||||||
@ -6,13 +5,8 @@ import showError from "../helpers/showError";
|
|||||||
import spotifyApi from "../initializations/spotifyApi";
|
import spotifyApi from "../initializations/spotifyApi";
|
||||||
|
|
||||||
function useSpotifyApi() {
|
function useSpotifyApi() {
|
||||||
const {
|
const { access_token, clientId, clientSecret, isLoggedIn, setAccess_token } =
|
||||||
access_token,
|
useContext(authContext);
|
||||||
clientId,
|
|
||||||
clientSecret,
|
|
||||||
isLoggedIn,
|
|
||||||
setAccess_token,
|
|
||||||
} = useContext(authContext);
|
|
||||||
const refreshToken = localStorage.getItem(LocalStorageKeys.refresh_token);
|
const refreshToken = localStorage.getItem(LocalStorageKeys.refresh_token);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -6,13 +6,17 @@ import showError from "../helpers/showError";
|
|||||||
|
|
||||||
function useSpotifyApiError(spotifyApi: SpotifyWebApi) {
|
function useSpotifyApiError(spotifyApi: SpotifyWebApi) {
|
||||||
const { setAccess_token, isLoggedIn } = useContext(authContext);
|
const { setAccess_token, isLoggedIn } = useContext(authContext);
|
||||||
return async (error: any | Error | TypeError) => {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return async (error: SpotifyApi.ErrorObject | any) => {
|
||||||
const isUnauthorized = error.message === "Unauthorized";
|
const isUnauthorized = error.message === "Unauthorized";
|
||||||
const status401 = error.status === 401;
|
const status401 = error.status === 401;
|
||||||
const bodyStatus401 = error.body.error.status === 401;
|
const bodyStatus401 = error.body.error.status === 401;
|
||||||
const noToken = error.body.error.message === "No token provided";
|
const noToken = error.body.error.message === "No token provided";
|
||||||
const expiredToken = error.body.error.message === "The access token expired";
|
const expiredToken = error.body.error.message === "The access token expired";
|
||||||
if ((isUnauthorized && isLoggedIn && status401) || ((noToken || expiredToken) && bodyStatus401)) {
|
if (
|
||||||
|
(isUnauthorized && isLoggedIn && status401) ||
|
||||||
|
((noToken || expiredToken) && bodyStatus401)
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
console.log(chalk.bgYellow.blackBright("Refreshing Access token"));
|
console.log(chalk.bgYellow.blackBright("Refreshing Access token"));
|
||||||
const {
|
const {
|
||||||
|
@ -1,19 +1,32 @@
|
|||||||
import { useEffect } from "react";
|
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 SpotifyWebApi from "spotify-web-api-node";
|
||||||
import useSpotifyApi from "./useSpotifyApi";
|
import useSpotifyApi from "./useSpotifyApi";
|
||||||
import useSpotifyApiError from "./useSpotifyApiError";
|
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>(
|
function useSpotifyInfiniteQuery<TQueryData = unknown>(
|
||||||
queryKey: QueryKey,
|
queryKey: QueryKey,
|
||||||
queryHandler: SpotifyQueryFn<TQueryData>,
|
queryHandler: SpotifyQueryFn<TQueryData>,
|
||||||
options: UseInfiniteQueryOptions<TQueryData, SpotifyApi.ErrorObject> = {}
|
options: UseInfiniteQueryOptions<TQueryData, SpotifyApi.ErrorObject> = {},
|
||||||
): UseInfiniteQueryResult<TQueryData, SpotifyApi.ErrorObject> {
|
): UseInfiniteQueryResult<TQueryData, SpotifyApi.ErrorObject> {
|
||||||
const spotifyApi = useSpotifyApi();
|
const spotifyApi = useSpotifyApi();
|
||||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||||
const query = useInfiniteQuery<TQueryData, SpotifyApi.ErrorObject>(queryKey, (pageArgs) => queryHandler(spotifyApi, pageArgs), options);
|
const query = useInfiniteQuery<TQueryData, SpotifyApi.ErrorObject>(
|
||||||
|
queryKey,
|
||||||
|
(pageArgs) => queryHandler(spotifyApi, pageArgs),
|
||||||
|
options,
|
||||||
|
);
|
||||||
const { isError, error } = query;
|
const { isError, error } = query;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -4,12 +4,21 @@ import SpotifyWebApi from "spotify-web-api-node";
|
|||||||
import useSpotifyApi from "./useSpotifyApi";
|
import useSpotifyApi from "./useSpotifyApi";
|
||||||
import useSpotifyApiError from "./useSpotifyApiError";
|
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>) {
|
function useSpotifyMutation<TData = unknown, TVariable = unknown>(
|
||||||
|
mutationFn: SpotifyMutationFn<TData, TVariable>,
|
||||||
|
options?: UseMutationOptions<TData, SpotifyApi.ErrorObject, TVariable>,
|
||||||
|
) {
|
||||||
const spotifyApi = useSpotifyApi();
|
const spotifyApi = useSpotifyApi();
|
||||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||||
const mutation = useMutation<TData, SpotifyApi.ErrorObject, TVariable>((arg) => mutationFn(spotifyApi, arg), options);
|
const mutation = useMutation<TData, SpotifyApi.ErrorObject, TVariable>(
|
||||||
|
(arg) => mutationFn(spotifyApi, arg),
|
||||||
|
options,
|
||||||
|
);
|
||||||
const { isError, error } = mutation;
|
const { isError, error } = mutation;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -9,14 +9,17 @@ type SpotifyQueryFn<TQueryData> = (spotifyApi: SpotifyWebApi) => Promise<TQueryD
|
|||||||
function useSpotifyQuery<TQueryData = unknown>(
|
function useSpotifyQuery<TQueryData = unknown>(
|
||||||
queryKey: QueryKey,
|
queryKey: QueryKey,
|
||||||
queryHandler: SpotifyQueryFn<TQueryData>,
|
queryHandler: SpotifyQueryFn<TQueryData>,
|
||||||
options: UseQueryOptions<TQueryData, SpotifyApi.ErrorObject> = {}
|
options: UseQueryOptions<TQueryData, SpotifyApi.ErrorObject> = {},
|
||||||
): UseQueryResult<TQueryData, SpotifyApi.ErrorObject> {
|
): UseQueryResult<TQueryData, SpotifyApi.ErrorObject> {
|
||||||
const spotifyApi = useSpotifyApi();
|
const spotifyApi = useSpotifyApi();
|
||||||
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
const handleSpotifyError = useSpotifyApiError(spotifyApi);
|
||||||
const query = useQuery<TQueryData, SpotifyApi.ErrorObject>(queryKey, ()=>queryHandler(spotifyApi), options);
|
const query = useQuery<TQueryData, SpotifyApi.ErrorObject>(
|
||||||
|
queryKey,
|
||||||
|
() => queryHandler(spotifyApi),
|
||||||
|
options,
|
||||||
|
);
|
||||||
const { isError, error } = query;
|
const { isError, error } = query;
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isError && error) {
|
if (isError && error) {
|
||||||
handleSpotifyError(error);
|
handleSpotifyError(error);
|
||||||
|
@ -5,40 +5,59 @@ import useSpotifyMutation from "./useSpotifyMutation";
|
|||||||
|
|
||||||
function useTrackReaction() {
|
function useTrackReaction() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: userSavedTracks } = useSpotifyInfiniteQuery<SpotifyApi.UsersSavedTracksResponse>(QueryCacheKeys.userSavedTracks, (spotifyApi, { pageParam }) =>
|
const { data: userSavedTracks } =
|
||||||
spotifyApi.getMySavedTracks({ limit: 50, offset: pageParam }).then((res) => res.body)
|
useSpotifyInfiniteQuery<SpotifyApi.UsersSavedTracksResponse>(
|
||||||
|
QueryCacheKeys.userSavedTracks,
|
||||||
|
(spotifyApi, { pageParam }) =>
|
||||||
|
spotifyApi
|
||||||
|
.getMySavedTracks({ limit: 50, offset: pageParam })
|
||||||
|
.then((res) => res.body),
|
||||||
);
|
);
|
||||||
const favoriteTracks = userSavedTracks?.pages
|
const favoriteTracks = userSavedTracks?.pages
|
||||||
?.map((page) => page.items)
|
?.map((page) => page.items)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.flat(1) as SpotifyApi.SavedTrackObject[] | undefined;
|
.flat(1) as SpotifyApi.SavedTrackObject[] | undefined;
|
||||||
|
|
||||||
function updateFunction(track: SpotifyApi.SavedTrackObject, old?: InfiniteData<SpotifyApi.UsersSavedTracksResponse>): InfiniteData<SpotifyApi.UsersSavedTracksResponse> {
|
function updateFunction(
|
||||||
|
track: SpotifyApi.SavedTrackObject,
|
||||||
|
old?: InfiniteData<SpotifyApi.UsersSavedTracksResponse>,
|
||||||
|
): InfiniteData<SpotifyApi.UsersSavedTracksResponse> {
|
||||||
const obj: typeof old = {
|
const obj: typeof old = {
|
||||||
pageParams: old?.pageParams ?? [],
|
pageParams: old?.pageParams ?? [],
|
||||||
pages:
|
pages:
|
||||||
old?.pages.map(
|
old?.pages.map((oldPage, index): SpotifyApi.UsersSavedTracksResponse => {
|
||||||
(oldPage, index): SpotifyApi.UsersSavedTracksResponse => {
|
|
||||||
const isTrackFavorite = isFavorite(track.track.id);
|
const isTrackFavorite = isFavorite(track.track.id);
|
||||||
if (index === 0 && !isTrackFavorite) {
|
if (index === 0 && !isTrackFavorite) {
|
||||||
return { ...oldPage, items: [...oldPage.items, track] };
|
return { ...oldPage, items: [...oldPage.items, track] };
|
||||||
} else if (isTrackFavorite) {
|
} else if (isTrackFavorite) {
|
||||||
return { ...oldPage, items: oldPage.items.filter((oldTrack) => oldTrack.track.id !== track.track.id) };
|
return {
|
||||||
|
...oldPage,
|
||||||
|
items: oldPage.items.filter(
|
||||||
|
(oldTrack) => oldTrack.track.id !== track.track.id,
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return oldPage;
|
return oldPage;
|
||||||
}
|
}) ?? [],
|
||||||
) ?? [],
|
|
||||||
};
|
};
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { mutate: reactToTrack } = useSpotifyMutation<{}, SpotifyApi.SavedTrackObject>(
|
const { mutate: reactToTrack } = useSpotifyMutation<
|
||||||
(spotifyApi, { track }) => spotifyApi[isFavorite(track.id) ? "removeFromMySavedTracks" : "addToMySavedTracks"]([track.id]).then((res) => res.body),
|
unknown,
|
||||||
|
SpotifyApi.SavedTrackObject
|
||||||
|
>(
|
||||||
|
(spotifyApi, { track }) =>
|
||||||
|
spotifyApi[
|
||||||
|
isFavorite(track.id) ? "removeFromMySavedTracks" : "addToMySavedTracks"
|
||||||
|
]([track.id]).then((res) => res.body),
|
||||||
{
|
{
|
||||||
onSuccess(_, track) {
|
onSuccess(_, track) {
|
||||||
queryClient.setQueryData<InfiniteData<SpotifyApi.UsersSavedTracksResponse>>(QueryCacheKeys.userSavedTracks, (old) => updateFunction(track, old));
|
queryClient.setQueryData<
|
||||||
|
InfiniteData<SpotifyApi.UsersSavedTracksResponse>
|
||||||
|
>(QueryCacheKeys.userSavedTracks, (old) => updateFunction(track, old));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
);
|
);
|
||||||
const favoriteTrackIds = favoriteTracks?.map((track) => track.track.id);
|
const favoriteTrackIds = favoriteTracks?.map((track) => track.track.id);
|
||||||
|
|
||||||
|
24
src/icons.ts
24
src/icons.ts
@ -1,18 +1,18 @@
|
|||||||
import _play from "../assets/play-solid.svg";
|
import _play from "../assets/play-solid.svg";
|
||||||
import _pause from "../assets/pause-solid.svg"
|
import _pause from "../assets/pause-solid.svg";
|
||||||
import _angleLeft from "../assets/angle-left-solid.svg"
|
import _angleLeft from "../assets/angle-left-solid.svg";
|
||||||
import _backward from "../assets/backward-solid.svg"
|
import _backward from "../assets/backward-solid.svg";
|
||||||
import _forward from "../assets/forward-solid.svg"
|
import _forward from "../assets/forward-solid.svg";
|
||||||
import _heartRegular from "../assets/heart-regular.svg"
|
import _heartRegular from "../assets/heart-regular.svg";
|
||||||
import _heart from "../assets/heart-solid.svg"
|
import _heart from "../assets/heart-solid.svg";
|
||||||
import _random from "../assets/random-solid.svg"
|
import _random from "../assets/random-solid.svg";
|
||||||
import _stop from "../assets/stop-solid.svg"
|
import _stop from "../assets/stop-solid.svg";
|
||||||
import _search from "../assets/search-solid.svg";
|
import _search from "../assets/search-solid.svg";
|
||||||
import _loadingSpinner from "../assets/loading-spinner.gif";
|
import _loadingSpinner from "../assets/loading-spinner.gif";
|
||||||
import _settingsCog from "../assets/setting-cog.svg"
|
import _settingsCog from "../assets/setting-cog.svg";
|
||||||
import _times from "../assets/times-solid.svg"
|
import _times from "../assets/times-solid.svg";
|
||||||
import _musicNode from "../assets/music-solid.svg"
|
import _musicNode from "../assets/music-solid.svg";
|
||||||
import _download from "../assets/download-solid.svg"
|
import _download from "../assets/download-solid.svg";
|
||||||
|
|
||||||
export const play = _play;
|
export const play = _play;
|
||||||
export const pause = _pause;
|
export const pause = _pause;
|
||||||
|
@ -12,6 +12,7 @@ import Search from "./components/Search";
|
|||||||
import SearchResultPlaylistCollection from "./components/SearchResultPlaylistCollection";
|
import SearchResultPlaylistCollection from "./components/SearchResultPlaylistCollection";
|
||||||
import SearchResultSongsCollection from "./components/SearchResultSongsCollection";
|
import SearchResultSongsCollection from "./components/SearchResultSongsCollection";
|
||||||
import Settings from "./components/Settings";
|
import Settings from "./components/Settings";
|
||||||
|
import Artist from "./components/Artist";
|
||||||
|
|
||||||
function Routes() {
|
function Routes() {
|
||||||
const { isLoggedIn } = useContext(authContext);
|
const { isLoggedIn } = useContext(authContext);
|
||||||
@ -42,6 +43,9 @@ function Routes() {
|
|||||||
<Route path="/library">
|
<Route path="/library">
|
||||||
<Library />
|
<Library />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/artist">
|
||||||
|
<Artist />
|
||||||
|
</Route>
|
||||||
<Route exact path="/search">
|
<Route exact path="/search">
|
||||||
<Search />
|
<Search />
|
||||||
</Route>
|
</Route>
|
||||||
|
2090
tsconfig.tsbuildinfo
2090
tsconfig.tsbuildinfo
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user