From e317c8346460b16a2ddd31dbf9849f99dceb75eb Mon Sep 17 00:00:00 2001 From: Rajit Khatri Date: Tue, 10 Sep 2024 19:50:05 -0400 Subject: [PATCH 1/3] Added the Now Playing UI code --- src/components/NowPlaying.tsx | 347 +++++++++++++++++++++++++++++----- src/css/app.module.scss | 4 +- 2 files changed, 307 insertions(+), 44 deletions(-) diff --git a/src/components/NowPlaying.tsx b/src/components/NowPlaying.tsx index 4ca72b1..616917c 100644 --- a/src/components/NowPlaying.tsx +++ b/src/components/NowPlaying.tsx @@ -2,13 +2,20 @@ import styles from "../css/app.module.scss"; import React from "react"; import getAudioFeatures from "../services/nowPlayingService"; import { AudioFeaturesResponse } from "../types/spotify-web-api"; +import { CircularProgressbar } from 'react-circular-progressbar'; +import 'react-circular-progressbar/dist/styles.css'; +import getRecommendations from "../services/dynamicRecommendationsService"; +import { GetRecommendationsInput, GetRecommendationsResponse } from "../types/spotify-web-api.d"; // All components are now classes // Extends means that the class is inheriting all the properties of a React component // The component does not accept any props because it does not take anything in. The second // object describes the state of the component, which contains audioFeatures and songURI. Each // state variable must be typed. -class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesResponse | {}, songURI: string}> { +class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesResponse | {}, + songURI: string, + queue: Array, + recommendations: GetRecommendationsResponse | {}}> { // The component has two states: one for holding the features // of the audio and another for holding the song's URI @@ -16,14 +23,18 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon audioFeatures: {}, songURI: "", songPlayerInfo: {}, + queue: Spicetify.LocalStorage.get("queue")?.split(',') || new Array, + recommendations: {}, } // After the component is mounted to the screen, make API // calls to get the features of the currenly playing song componentDidMount = () => { this.setAudioFeatures(); + this.generateRecommendations(); } + // Function for updating the audio features of the song setAudioFeatures = () => { if (!Spicetify.Player.data || this.state.songURI == Spicetify.Player.data.item.uri) { @@ -41,14 +52,49 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon apiCall(); } - playIcon = ; + generateRecommendations = async () => { + let apiOptions = new GetRecommendationsInput(); + apiOptions.data.seed_tracks = this.state.queue.toString(); + var recommendations = await getRecommendations(apiOptions); + this.setState({ + recommendations: recommendations, + }); + }; + + addToQueue = (event?: Event & {data: number}) => { + if (!event || !Spicetify.Player.data || this.state.queue.includes(Spicetify.Player.data.item.uri.split(":")[2])) { + return; + } + + let progressPercentage = (event.data / Spicetify.Player.data.item.duration.milliseconds) * 100; + if (progressPercentage < 50) { + return; + } - nowPlayingStyles = { - primaryTrackInfo: {} + let newQueue = this.state.queue.slice(); + if (this.state.queue.length == 5) { + newQueue.shift(); + } + + newQueue.push(Spicetify.Player.data.item.uri.split(":")[2]); + this.setState({ + queue: newQueue, + }, () => { + if (Spicetify.LocalStorage.get("queue") == this.state.queue.toString()) { + return; + } + Spicetify.LocalStorage.set("queue", this.state.queue.toString()); + this.generateRecommendations(); + }); } + + playIcon = ; + + render() { // Add an event listener for when the song is changed Spicetify.Player.addEventListener("songchange", this.setAudioFeatures); + Spicetify.Player.addEventListener("onprogress", this.addToQueue); return ( <> {/* @@ -112,12 +158,44 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon {/* Recommendation #1 */}
{/* Recommendation cover */} - + 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][0].album.images[0].url : + ""}/> {/* Recommendation track details */}
-
{"Ray of light"}
-
{"Madonna"}
-
{"Ray of light"}
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][0].name : + ""} +
+ {Object.keys(this.state.recommendations).length > 0 ? ( () => { + // Get all the artists + const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][0].artists; + let trackAritistsInnnerHTML = ""; + + // Check if there are any artists + if (trackArtists) { + // Display all the artists + for (const artist of trackArtists) { + trackAritistsInnnerHTML += (artist.name + ", ") + } + if(trackAritistsInnnerHTML.length > 0) { + trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); + } + + return (
+ {trackAritistsInnnerHTML} +
); + } else { + return <>; + } + + })() :
} +
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][0].album.name : + ""} +
{/* Play icon */} {this.playIcon} @@ -125,49 +203,219 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon {/* Recommendation #2 */}
{/* Recommendation cover */} - + 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][1].album.images[0].url : + ""}/> {/* Recommendation track details */}
-
{"Poker Face"}
-
{"Lady Gaga"}
-
{"The Fame"}
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][1].name : + ""} +
+
+ {Object.keys(this.state.recommendations).length > 0 ? ( () => { + // Get all the artists + const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][1].artists; + let trackAritistsInnnerHTML = ""; + + // Check if there are any artists + if (trackArtists) { + // Display all the artists + for (const artist of trackArtists) { + trackAritistsInnnerHTML += (artist.name + ", ") + } + if(trackAritistsInnnerHTML.length > 0) { + trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); + } + + return (
+ {trackAritistsInnnerHTML} +
); + } else { + return <>; + } + + })() :
} +
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][1].album.name : + ""} +
{/* Play icon*/} {this.playIcon}
- + 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][2].album.images[0].url : + ""}/>
-
{"Like a Prayer"}
-
{"Madonna"}
-
{"Like a Prayer"}
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][2].name : + ""} +
+
+ {Object.keys(this.state.recommendations).length > 0 ? ( () => { + // Get all the artists + const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][2].artists; + let trackAritistsInnnerHTML = ""; + + // Check if there are any artists + if (trackArtists) { + // Display all the artists + for (const artist of trackArtists) { + trackAritistsInnnerHTML += (artist.name + ", ") + } + if(trackAritistsInnnerHTML.length > 0) { + trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); + } + + return (
+ {trackAritistsInnnerHTML} +
); + } else { + return <>; + } + + })() :
} +
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][2].album.name : + ""} +
{this.playIcon}
- + 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][3].album.images[0].url : + ""}/>
-
{"Anti Hero"}
-
{"Taylor Swift"}
-
{"Midnights"}
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][3].name : + ""} +
+
+ {Object.keys(this.state.recommendations).length > 0 ? ( () => { + // Get all the artists + const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][3].artists; + let trackAritistsInnnerHTML = ""; + + // Check if there are any artists + if (trackArtists) { + // Display all the artists + for (const artist of trackArtists) { + trackAritistsInnnerHTML += (artist.name + ", ") + } + if(trackAritistsInnnerHTML.length > 0) { + trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); + } + + return (
+ {trackAritistsInnnerHTML} +
); + } else { + return <>; + } + + })() :
} +
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][3].album.name : + ""} +
{this.playIcon}
- + 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][4].album.images[0].url : + ""}/>
-
{"Lover"}
-
{"Taylor Swift"}
-
{"Lover"}
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][4].name : + ""} +
+
+ {Object.keys(this.state.recommendations).length > 0 ? ( () => { + // Get all the artists + const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][4].artists; + let trackAritistsInnnerHTML = ""; + + // Check if there are any artists + if (trackArtists) { + // Display all the artists + for (const artist of trackArtists) { + trackAritistsInnnerHTML += (artist.name + ", ") + } + if(trackAritistsInnnerHTML.length > 0) { + trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); + } + + return (
+ {trackAritistsInnnerHTML} +
); + } else { + return <>; + } + + })() :
} +
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][4].album.name : + ""} +
{this.playIcon}
- + 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][5].album.images[0].url : + ""}/>
-
{"Shake It Off"}
-
{"Taylor Swift"}
-
{"1989"}
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][5].name : + ""} +
+
+ {Object.keys(this.state.recommendations).length > 0 ? ( () => { + // Get all the artists + const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][5].artists; + let trackAritistsInnnerHTML = ""; + + // Check if there are any artists + if (trackArtists) { + // Display all the artists + for (const artist of trackArtists) { + trackAritistsInnnerHTML += (artist.name + ", ") + } + if(trackAritistsInnnerHTML.length > 0) { + trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); + } + + return (
+ {trackAritistsInnnerHTML} +
); + } else { + return <>; + } + + })() :
} +
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][5].album.name : + ""} +
{this.playIcon}
@@ -178,38 +426,49 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon {/* Statistic #1 */}
-
{"Danceability"}
-
{"75"}
+
{"Danceability"}
+
+ {Math.round(parseFloat((this.state.audioFeatures as AudioFeaturesResponse)["danceability"]) * 100)} +
- {/*
*/} +
{/* Statistic #2 */}
-
{"Energy"}
-
{"55"}
+
{"Energy"}
+
+ {Math.round(parseFloat((this.state.audioFeatures as AudioFeaturesResponse)["energy"]) * 100)} +
- {/*
*/} +
{/* Statistic #3 */}
-
{"Acousticness"}
-
{"65"}
+
{"Acousticness"}
+
+ {Math.round(parseFloat((this.state.audioFeatures as AudioFeaturesResponse)["acousticness"]) * 100)} +
- {/*
*/} +
{/* Statistic #4 */}
-
{"Loudness"}
-
{"85"}
+
{"Loudness"}
+
+ {Math.round(parseFloat((this.state.audioFeatures as AudioFeaturesResponse)["loudness"]))} + + {"dB"} + +
{/*
*/} @@ -218,8 +477,8 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon {/* Statistic #5 */}
-
{"Key"}
-
{"95"}
+
{"Key"}
+
{(this.state.audioFeatures as AudioFeaturesResponse)["key"]}
{/*
*/} @@ -228,8 +487,10 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon {/* Statistic #6 */}
-
{"Tempo"}
-
{"65"}
+
{"Tempo"}
+
+ {Math.round(parseFloat((this.state.audioFeatures as AudioFeaturesResponse)["tempo"]))} +
{/*
*/} diff --git a/src/css/app.module.scss b/src/css/app.module.scss index e0ec7ec..fd9ad72 100644 --- a/src/css/app.module.scss +++ b/src/css/app.module.scss @@ -40,7 +40,7 @@ } .recommendationsBlock { - width: 70%; + width: auto; border-radius: 5px; margin-top: 8px; } @@ -91,6 +91,8 @@ justify-content: flex-end; align-self: center; margin-left: auto; + width: 100px; + height: 100px; } .trackContainer { From fb131a0e7df1d0705d0beaad590fc3819e668105 Mon Sep 17 00:00:00 2001 From: Rajit Khatri Date: Tue, 17 Sep 2024 09:56:29 -0400 Subject: [PATCH 2/3] Completed the initial draft of the now playing page --- src/components/NowPlaying.tsx | 64 +++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/src/components/NowPlaying.tsx b/src/components/NowPlaying.tsx index 616917c..6e050f1 100644 --- a/src/components/NowPlaying.tsx +++ b/src/components/NowPlaying.tsx @@ -7,18 +7,12 @@ import 'react-circular-progressbar/dist/styles.css'; import getRecommendations from "../services/dynamicRecommendationsService"; import { GetRecommendationsInput, GetRecommendationsResponse } from "../types/spotify-web-api.d"; -// All components are now classes -// Extends means that the class is inheriting all the properties of a React component -// The component does not accept any props because it does not take anything in. The second -// object describes the state of the component, which contains audioFeatures and songURI. Each -// state variable must be typed. + class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesResponse | {}, songURI: string, queue: Array, recommendations: GetRecommendationsResponse | {}}> { - // The component has two states: one for holding the features - // of the audio and another for holding the song's URI state = { audioFeatures: {}, songURI: "", @@ -27,14 +21,11 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon recommendations: {}, } - // After the component is mounted to the screen, make API - // calls to get the features of the currenly playing song componentDidMount = () => { this.setAudioFeatures(); this.generateRecommendations(); } - // Function for updating the audio features of the song setAudioFeatures = () => { if (!Spicetify.Player.data || this.state.songURI == Spicetify.Player.data.item.uri) { @@ -89,17 +80,59 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon } playIcon = ; - + recommendationIndexes = Array.from(Array(6).keys()); + recommendationItem = (i : number) => + (
+ {/* Recommendation cover */} + 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][i].album.images[0].url : + ""}/> + {/* Recommendation track details */} +
+
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][i].name : + ""} +
+ {Object.keys(this.state.recommendations).length > 0 ? ( () => { + // Get all the artists + const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][i].artists; + let trackAritistsInnnerHTML = ""; + + // Check if there are any artists + if (trackArtists) { + // Display all the artists + for (const artist of trackArtists) { + trackAritistsInnnerHTML += (artist.name + ", ") + } + if(trackAritistsInnnerHTML.length > 0) { + trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); + } + + return (
+ {trackAritistsInnnerHTML} +
); + } else { + return <>; + } + + })() :
} +
+ {Object.keys(this.state.recommendations).length > 0 ? + (this.state.recommendations as GetRecommendationsResponse)["tracks"][i].album.name : + ""} +
+
+ {/* Play icon */} + {this.playIcon} +
); + recommendationItems = this.recommendationIndexes.map((i) => this.recommendationItem(i)); render() { - // Add an event listener for when the song is changed Spicetify.Player.addEventListener("songchange", this.setAudioFeatures); Spicetify.Player.addEventListener("onprogress", this.addToQueue); return ( <> - {/* - {JSON.stringify(this.state.audioFeatures)} - */}
{/* Now playing rectangle sidebar */}
@@ -421,6 +454,7 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon
+ {/* Stats block */}
{/* Statistic #1 */} From 08ef269c2d078d3380273545024dacbcf896e4e1 Mon Sep 17 00:00:00 2001 From: Rajit Khatri Date: Fri, 27 Sep 2024 10:11:53 -0400 Subject: [PATCH 3/3] Updated the now playing page with advanced features --- package-lock.json | 116 ++++- package.json | 5 +- src/components/DynamicRecommendations.tsx | 70 +-- src/components/NowPlaying.tsx | 498 +++++----------------- src/components/RecommendedTrack.tsx | 64 +++ src/css/app.module.scss | 188 ++++++-- 6 files changed, 488 insertions(+), 453 deletions(-) create mode 100644 src/components/RecommendedTrack.tsx diff --git a/package-lock.json b/package-lock.json index 1aace62..5bebc9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,10 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@spotify/web-api-ts-sdk": "^1.2.0" + "@spotify/web-api-ts-sdk": "^1.2.0", + "@types/jquery": "^3.5.31", + "jquery": "^3.7.1", + "react-circular-progressbar": "^2.1.0" }, "devDependencies": { "@types/react": "^18.3.4", @@ -44,6 +47,15 @@ "resolved": "https://registry.npmjs.org/@spotify/web-api-ts-sdk/-/web-api-ts-sdk-1.2.0.tgz", "integrity": "sha512-JUaebva3Ohwo5I5tuTqyW/FKGOMbb40YevJMySAOINRxP7qQ/AMjBzfJx0zeO6yS+wAPfQSoGNsZaUggHw8vsA==" }, + "node_modules/@types/jquery": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.31.tgz", + "integrity": "sha512-rf/iB+cPJ/YZfMwr+FVuQbm7IaWC4y3FVYfVDxRGqmUCFjjPII0HWaP0vTPJGp6m4o13AXySCcMbWfrWtBFAKw==", + "license": "MIT", + "dependencies": { + "@types/sizzle": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -69,6 +81,12 @@ "@types/react": "*" } }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "license": "MIT" + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1186,6 +1204,19 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT", + "peer": true + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -1263,6 +1294,19 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -1587,6 +1631,28 @@ "dev": true, "optional": true }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-circular-progressbar": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz", + "integrity": "sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g==", + "license": "MIT", + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1950,6 +2016,14 @@ "resolved": "https://registry.npmjs.org/@spotify/web-api-ts-sdk/-/web-api-ts-sdk-1.2.0.tgz", "integrity": "sha512-JUaebva3Ohwo5I5tuTqyW/FKGOMbb40YevJMySAOINRxP7qQ/AMjBzfJx0zeO6yS+wAPfQSoGNsZaUggHw8vsA==" }, + "@types/jquery": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.31.tgz", + "integrity": "sha512-rf/iB+cPJ/YZfMwr+FVuQbm7IaWC4y3FVYfVDxRGqmUCFjjPII0HWaP0vTPJGp6m4o13AXySCcMbWfrWtBFAKw==", + "requires": { + "@types/sizzle": "*" + } + }, "@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -1975,6 +2049,11 @@ "@types/react": "*" } }, + "@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==" + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2673,6 +2752,17 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -2731,6 +2821,15 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -2949,6 +3048,21 @@ "dev": true, "optional": true }, + "react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-circular-progressbar": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz", + "integrity": "sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g==", + "requires": {} + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", diff --git a/package.json b/package.json index fbf46e0..2eb0d3b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "spicetify-creator": "^1.0.17" }, "dependencies": { - "@spotify/web-api-ts-sdk": "^1.2.0" + "@spotify/web-api-ts-sdk": "^1.2.0", + "@types/jquery": "^3.5.31", + "jquery": "^3.7.1", + "react-circular-progressbar": "^2.1.0" } } diff --git a/src/components/DynamicRecommendations.tsx b/src/components/DynamicRecommendations.tsx index 320f63c..336f826 100644 --- a/src/components/DynamicRecommendations.tsx +++ b/src/components/DynamicRecommendations.tsx @@ -3,20 +3,26 @@ import React from "react"; import getRecommendations from "../services/dynamicRecommendationsService"; import { GetRecommendationsInput, GetRecommendationsResponse } from "../types/spotify-web-api.d"; import getID from './../services/common'; +import RecommendedTrack from "./RecommendedTrack"; -class DynamicRecommendations extends React.Component<{}, {songQueue: Array, artistQueue: Array, recTarget: string, recommendations: GetRecommendationsResponse | {}}> { +class DynamicRecommendations extends React.Component<{recTargetProp : string}, {songQueue: Array, artistQueue: Array, recTarget: string, recommendations: GetRecommendationsResponse | {}}> { + state = { - songQueue: Spicetify.LocalStorage.get("songQueue")?.split(',') || new Array, - artistQueue: Spicetify.LocalStorage.get("artistQueue")?.split(',') || new Array, - recTarget: "songs", - recommendations: {}, + songQueue: Spicetify.LocalStorage.get("songQueue")?.split(',') || new Array, // Song in the queue + artistQueue: Spicetify.LocalStorage.get("artistQueue")?.split(',') || new Array, // Artists in the queue + recTarget: this.props.recTargetProp, // Get recommendations based + // on songs or artists queue + recommendations: {}, // Recommendations list } componentDidMount = () => { this.generateRecommendations(); } + // Generate recommendations by sending a request to the spotify API generateRecommendations = async () => { + + // Prepare the recommendations to send to the server let apiOptions = new GetRecommendationsInput(); if (this.state.recTarget == "songs") { apiOptions.data.seed_tracks = this.state.songQueue.toString(); @@ -24,13 +30,15 @@ class DynamicRecommendations extends React.Component<{}, {songQueue: Array { if (!event || !Spicetify.Player.data) { return; @@ -41,10 +49,12 @@ class DynamicRecommendations extends React.Component<{}, {songQueue: Array { let curSongID = getID(Spicetify.Player.data.item.uri); if (this.state.songQueue && this.state.songQueue[this.state.songQueue.length-1] == curSongID) { @@ -74,6 +84,7 @@ class DynamicRecommendations extends React.Component<{}, {songQueue: Array { if (!Spicetify.Player.data.item.artists) { return false; @@ -92,6 +103,7 @@ class DynamicRecommendations extends React.Component<{}, {songQueue: Array { if (!Spicetify.Player.data.item.artists || !this.shouldArtistQueueBeUpdated()) { return; @@ -123,31 +135,39 @@ class DynamicRecommendations extends React.Component<{}, {songQueue: Array { - if (this.state.recTarget == "songs") { - this.setState({ - recTarget: "artists", - }, () => this.generateRecommendations()); + componentDidUpdate(prevProps: Readonly<{ recTargetProp: string; }>, + prevState: Readonly<{ songQueue: Array; artistQueue: Array; recTarget: string; recommendations: GetRecommendationsResponse | {}; }>, snapshot?: any): void { + if (prevProps.recTargetProp != this.props.recTargetProp) { + this.generateRecommendations(); } - else if (this.state.recTarget == "artists") { - this.setState({ - recTarget: "songs", - }, () => this.generateRecommendations()); - } - }; + } render() { Spicetify.Player.addEventListener("onprogress", this.addToQueue); return ( <> - - {"songQueue: " + String(this.state.songQueue) + "\n"} - {"artistQueue: " + String(this.state.artistQueue) + "\n"} - {JSON.stringify(Object.keys(this.state.recommendations).length != 0 ? (this.state.recommendations as GetRecommendationsResponse)["tracks"][0].name : {})} - - +
+
+
{"Song Recommendations"}
+
+
{this.props.recTargetProp}
+
+
+ {function(recommendations : GetRecommendationsResponse | {}) { + let recommendedTracksHTML = []; + for (let i = 0; i < 6; i++) { + let recommendedSong = 0 ? (recommendations as GetRecommendationsResponse)["tracks"][i].album.images[0].url : ""} + songAlbum={Object.keys(recommendations).length > 0 ? (recommendations as GetRecommendationsResponse)["tracks"][i].album.name : ""} + songName={Object.keys(recommendations).length > 0 ? (recommendations as GetRecommendationsResponse)["tracks"][i].name : ""} + songArtists={Object.keys(recommendations).length > 0 ? (recommendations as GetRecommendationsResponse)["tracks"][i].artists.map((artist) => artist.name):[]} + key={i}> + ; + recommendedTracksHTML.push(recommendedSong); + } + return recommendedTracksHTML; + }(this.state.recommendations)} +
+
); } diff --git a/src/components/NowPlaying.tsx b/src/components/NowPlaying.tsx index 6e050f1..b736d7e 100644 --- a/src/components/NowPlaying.tsx +++ b/src/components/NowPlaying.tsx @@ -6,33 +6,36 @@ import { CircularProgressbar } from 'react-circular-progressbar'; import 'react-circular-progressbar/dist/styles.css'; import getRecommendations from "../services/dynamicRecommendationsService"; import { GetRecommendationsInput, GetRecommendationsResponse } from "../types/spotify-web-api.d"; - +import DynamicRecommendations from "./DynamicRecommendations"; class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesResponse | {}, songURI: string, - queue: Array, - recommendations: GetRecommendationsResponse | {}}> { + recTarget: string}> { state = { - audioFeatures: {}, - songURI: "", - songPlayerInfo: {}, - queue: Spicetify.LocalStorage.get("queue")?.split(',') || new Array, - recommendations: {}, + audioFeatures: {}, // Features of the currently playing song (name, artist, stats) + songURI: "", // URI of the currently playing song + recTarget: "songs", // Recommendations based on either songs or artist + // songPlayerInfo: {}, + // queue: Spicetify.LocalStorage.get("queue")?.split(',') || new Array, + // recommendations: {}, } componentDidMount = () => { this.setAudioFeatures(); - this.generateRecommendations(); } setAudioFeatures = () => { - if (!Spicetify.Player.data || this.state.songURI == Spicetify.Player.data.item.uri) { + // Check if there is no currently playing song or + // if the info of the song is currently being displayed + if (!Spicetify.Player.data || this.state.songURI == Spicetify.Player.data.item.uri) { return; } + this.state.songURI = Spicetify.Player.data.item.uri; + // API call for getting song info const apiCall = async () => { const currentAudioFeatures = await getAudioFeatures(this.state.songURI || ""); this.setState({ @@ -40,106 +43,34 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon }); } + // Make the API call apiCall(); } - - generateRecommendations = async () => { - let apiOptions = new GetRecommendationsInput(); - apiOptions.data.seed_tracks = this.state.queue.toString(); - var recommendations = await getRecommendations(apiOptions); - this.setState({ - recommendations: recommendations, - }); - }; - addToQueue = (event?: Event & {data: number}) => { - if (!event || !Spicetify.Player.data || this.state.queue.includes(Spicetify.Player.data.item.uri.split(":")[2])) { - return; - } - - let progressPercentage = (event.data / Spicetify.Player.data.item.duration.milliseconds) * 100; - if (progressPercentage < 50) { - return; + // Change the recommendation target + changeRecTarget = () => { + if (this.state.recTarget == "songs") { + this.setState({ + recTarget: "artists", + }); } - - let newQueue = this.state.queue.slice(); - if (this.state.queue.length == 5) { - newQueue.shift(); + else if (this.state.recTarget == "artists") { + this.setState({ + recTarget: "songs", + }); } - - newQueue.push(Spicetify.Player.data.item.uri.split(":")[2]); - this.setState({ - queue: newQueue, - }, () => { - if (Spicetify.LocalStorage.get("queue") == this.state.queue.toString()) { - return; - } - Spicetify.LocalStorage.set("queue", this.state.queue.toString()); - this.generateRecommendations(); - }); - } - - playIcon = ; - recommendationIndexes = Array.from(Array(6).keys()); - recommendationItem = (i : number) => - (
- {/* Recommendation cover */} - 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][i].album.images[0].url : - ""}/> - {/* Recommendation track details */} -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][i].name : - ""} -
- {Object.keys(this.state.recommendations).length > 0 ? ( () => { - // Get all the artists - const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][i].artists; - let trackAritistsInnnerHTML = ""; - - // Check if there are any artists - if (trackArtists) { - // Display all the artists - for (const artist of trackArtists) { - trackAritistsInnnerHTML += (artist.name + ", ") - } - if(trackAritistsInnnerHTML.length > 0) { - trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); - } - - return (
- {trackAritistsInnnerHTML} -
); - } else { - return <>; - } - - })() :
} -
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][i].album.name : - ""} -
-
- {/* Play icon */} - {this.playIcon} -
); - recommendationItems = this.recommendationIndexes.map((i) => this.recommendationItem(i)); + }; render() { + Spicetify.Player.addEventListener("songchange", this.setAudioFeatures); - Spicetify.Player.addEventListener("onprogress", this.addToQueue); + return ( - <> + <>
- {/* Now playing rectangle sidebar */}
- {/* Primary track info */}
{/* Track cover */} - {/* Check if there is an image associated with the track */} {Spicetify.Player.data.item.images ? Spicetify.Player.data.item.images.length > 0 ? @@ -149,7 +80,14 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon {/* Track title */} + fontWeight: "530", + textOverflow: "ellipsis", + overflow: "hidden", + whiteSpace: "nowrap", + textAlign: "center", + alignContent: "center", + width: "250px", + color: "white"}}> {Spicetify.Player.data.item.name} @@ -171,366 +109,136 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon } return - {trackAritistsInnnerHTML} + marginBottom: "2px", + textOverflow: "ellipsis", + width: "250px", + textAlign: "center",}}> + {trackAritistsInnnerHTML} } else { return <>; } - })()} {/* Track album */} - + {Spicetify.Player.data.item.album.name}
- {/* Recommendations block */} -
- {/* Recommendation #1 */} -
- {/* Recommendation cover */} - 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][0].album.images[0].url : - ""}/> - {/* Recommendation track details */} -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][0].name : - ""} -
- {Object.keys(this.state.recommendations).length > 0 ? ( () => { - // Get all the artists - const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][0].artists; - let trackAritistsInnnerHTML = ""; - - // Check if there are any artists - if (trackArtists) { - // Display all the artists - for (const artist of trackArtists) { - trackAritistsInnnerHTML += (artist.name + ", ") - } - if(trackAritistsInnnerHTML.length > 0) { - trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); - } - - return (
- {trackAritistsInnnerHTML} -
); - } else { - return <>; - } - - })() :
} -
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][0].album.name : - ""} -
-
- {/* Play icon */} - {this.playIcon} -
- {/* Recommendation #2 */} -
- {/* Recommendation cover */} - 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][1].album.images[0].url : - ""}/> - {/* Recommendation track details */} -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][1].name : - ""} -
-
- {Object.keys(this.state.recommendations).length > 0 ? ( () => { - // Get all the artists - const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][1].artists; - let trackAritistsInnnerHTML = ""; - - // Check if there are any artists - if (trackArtists) { - // Display all the artists - for (const artist of trackArtists) { - trackAritistsInnnerHTML += (artist.name + ", ") - } - if(trackAritistsInnnerHTML.length > 0) { - trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); - } - - return (
- {trackAritistsInnnerHTML} -
); - } else { - return <>; - } - - })() :
} -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][1].album.name : - ""} -
-
- {/* Play icon*/} - {this.playIcon} -
-
- 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][2].album.images[0].url : - ""}/> -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][2].name : - ""} -
-
- {Object.keys(this.state.recommendations).length > 0 ? ( () => { - // Get all the artists - const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][2].artists; - let trackAritistsInnnerHTML = ""; - - // Check if there are any artists - if (trackArtists) { - // Display all the artists - for (const artist of trackArtists) { - trackAritistsInnnerHTML += (artist.name + ", ") - } - if(trackAritistsInnnerHTML.length > 0) { - trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); - } - - return (
- {trackAritistsInnnerHTML} -
); - } else { - return <>; - } - - })() :
} -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][2].album.name : - ""} -
-
- {this.playIcon} -
-
- 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][3].album.images[0].url : - ""}/> -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][3].name : - ""} -
-
- {Object.keys(this.state.recommendations).length > 0 ? ( () => { - // Get all the artists - const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][3].artists; - let trackAritistsInnnerHTML = ""; - - // Check if there are any artists - if (trackArtists) { - // Display all the artists - for (const artist of trackArtists) { - trackAritistsInnnerHTML += (artist.name + ", ") - } - if(trackAritistsInnnerHTML.length > 0) { - trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); - } - - return (
- {trackAritistsInnnerHTML} -
); - } else { - return <>; - } - - })() :
} -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][3].album.name : - ""} -
-
- {this.playIcon} -
-
- 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][4].album.images[0].url : - ""}/> -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][4].name : - ""} -
-
- {Object.keys(this.state.recommendations).length > 0 ? ( () => { - // Get all the artists - const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][4].artists; - let trackAritistsInnnerHTML = ""; - - // Check if there are any artists - if (trackArtists) { - // Display all the artists - for (const artist of trackArtists) { - trackAritistsInnnerHTML += (artist.name + ", ") - } - if(trackAritistsInnnerHTML.length > 0) { - trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); - } - - return (
- {trackAritistsInnnerHTML} -
); - } else { - return <>; - } - - })() :
} -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][4].album.name : - ""} -
-
- {this.playIcon} -
-
- 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][5].album.images[0].url : - ""}/> -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][5].name : - ""} -
-
- {Object.keys(this.state.recommendations).length > 0 ? ( () => { - // Get all the artists - const trackArtists = (this.state.recommendations as GetRecommendationsResponse)["tracks"][5].artists; - let trackAritistsInnnerHTML = ""; - - // Check if there are any artists - if (trackArtists) { - // Display all the artists - for (const artist of trackArtists) { - trackAritistsInnnerHTML += (artist.name + ", ") - } - if(trackAritistsInnnerHTML.length > 0) { - trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); - } - - return (
- {trackAritistsInnnerHTML} -
); - } else { - return <>; - } - - })() :
} -
-
- {Object.keys(this.state.recommendations).length > 0 ? - (this.state.recommendations as GetRecommendationsResponse)["tracks"][5].album.name : - ""} -
-
- {this.playIcon} -
+
+
{/* Stats block */} +
+ {"Song Statistics"} +
{/* Statistic #1 */}
-
{"Danceability"}
-
+
+ {"Danceability"} +
+
{Math.round(parseFloat((this.state.audioFeatures as AudioFeaturesResponse)["danceability"]) * 100)}
- + +
{/* Statistic #2 */}
-
{"Energy"}
-
+
+ {"Energy"} +
+
{Math.round(parseFloat((this.state.audioFeatures as AudioFeaturesResponse)["energy"]) * 100)}
- + +
{/* Statistic #3 */}
-
{"Acousticness"}
-
+
+ {"Acousticness"} +
+
{Math.round(parseFloat((this.state.audioFeatures as AudioFeaturesResponse)["acousticness"]) * 100)}
- + +
{/* Statistic #4 */}
-
{"Loudness"}
-
+
+ {"Loudness"} +
+
{Math.round(parseFloat((this.state.audioFeatures as AudioFeaturesResponse)["loudness"]))} {"dB"} -
-
-
- {/*
*/} +
{/* Statistic #5 */}
-
{"Key"}
-
{(this.state.audioFeatures as AudioFeaturesResponse)["key"]}
-
-
- {/*
*/} +
+ {"Key"} +
+
+ {(this.state.audioFeatures as AudioFeaturesResponse)["key"]} +
{/* Statistic #6 */}
-
{"Tempo"}
-
- {Math.round(parseFloat((this.state.audioFeatures as AudioFeaturesResponse)["tempo"]))} -
-
-
- {/*
*/} +
+ {"Tempo"} +
+
+ {Math.round(parseFloat((this.state.audioFeatures as AudioFeaturesResponse)["tempo"]))} +
+
+
+ {"Settings"}
+
+ {"Show recommendations by: "} + {/* */} + +
+
); } diff --git a/src/components/RecommendedTrack.tsx b/src/components/RecommendedTrack.tsx new file mode 100644 index 0000000..4c81dfb --- /dev/null +++ b/src/components/RecommendedTrack.tsx @@ -0,0 +1,64 @@ +import styles from "../css/app.module.scss"; +import React, { ReactNode } from "react"; + +class RecommendedTrack extends React.Component<{songCover: string, + songName: string, + songArtists: string[], + songAlbum: string}, + {paddingRight: string}> { + + playIcon = ; + + render() { + return ( +
+ +
+
+ {this.props.songName.length > 12 ? +

+ {this.props.songName} +          + {this.props.songName} +          +

: +

{this.props.songName}

+ } +
+
+ {Object.keys(this.props.songArtists).length > 0 ? ( () => { + // Get all the artists + const trackArtists = this.props.songArtists; + let trackAritistsInnnerHTML = ""; + + // Check if there are any artists + if (trackArtists) { + // Display all the artists + for (const artist of trackArtists) { + trackAritistsInnnerHTML += (artist + ", ") + } + if(trackAritistsInnnerHTML.length > 0) { + trackAritistsInnnerHTML = trackAritistsInnnerHTML.substring(0, trackAritistsInnnerHTML.length - 2); + } + + return (
+ {trackAritistsInnnerHTML} +
); + } else { + return <>; + } + + })() :
} +
+
+ {this.props.songAlbum} +
+
+ {/* Play icon*/} + {this.playIcon} +
+ ); + } +} + +export default RecommendedTrack; \ No newline at end of file diff --git a/src/css/app.module.scss b/src/css/app.module.scss index fd9ad72..a20ca3a 100644 --- a/src/css/app.module.scss +++ b/src/css/app.module.scss @@ -3,9 +3,21 @@ word-wrap: break-word; } +.topBar { + display: flex; + flex-direction: row; +} + +.nowPlayingSidebar { + width: 300px; + display: flex; + flex-direction: column; + align-items: center; +} + .trackInfoPrimary { width: 280px; - margin-top: 25px; + margin-top: 35px; display: flex; flex-direction: column; justify-content: center; @@ -18,33 +30,17 @@ height: 250px; } -.nowPlayingSidebar { - width: 300px; - display: flex; - flex-direction: column; - align-items: center; -} - .statsBlock { width: 96.5%; height: 290px; margin-top: 40px; margin: 20px; + margin-right: 0px; margin-bottom: 0px; + margin-left: 0px; border-radius: 5px; } -.topBar { - display: flex; - flex-direction: row; -} - -.recommendationsBlock { - width: auto; - border-radius: 5px; - margin-top: 8px; -} - .statsBlock { display: grid; grid-template-columns: 1fr 1fr 1fr; @@ -52,6 +48,8 @@ row-gap: 10px; column-gap: 10px; padding: 10px; + padding-right: 0px; + padding-top: 0px; } .statContainer { @@ -69,15 +67,6 @@ font-size: larger; } -.recommendationsBlock { - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: 1fr 1fr 1fr; - row-gap: 20px; - column-gap: 20px; - padding: 20px; -} - .statLabel { font-weight: 600; } @@ -95,6 +84,23 @@ height: 100px; } +.recommendationsSection { + width: 750px; + padding-bottom: 20px; + padding-left: 20px; + padding-right: 20px; +} + +.recommendationsBlock { + border-radius: 5px; + margin-top: 8px; + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(3, 1fr); + row-gap: 20px; + column-gap: 20px; +} + .trackContainer { display: flex; background-image: linear-gradient(to right, rgb(43, 43, 43), rgb(79, 79, 79)); @@ -111,9 +117,9 @@ } .trackName { - color: white; - font-size: 20px; - font-weight: 600; + width: 120px; + overflow: hidden; + white-space: nowrap; } .playIcon { @@ -121,4 +127,124 @@ height: 40px; margin-left: auto; align-self: center; +} + +.changeRecTragetBtn { + border-radius: 10px; + font-size: large; + padding: 10px; + background-color: rgb(81, 126, 97); + color: white; +} + +.trackNameText { + color: white; + font-size: 20px; + font-weight: 600; + white-space: nowrap; + position: relative; + display: inline-block; + transition-delay: 2s; + transition-property: right; +} + +.leftSideBar { + display: flex; + flex-direction: column; +} + +// .scrollTitle { +// -webkit-animation: linear infinite; +// -webkit-animation-name: run; +// -webkit-animation-duration: 5s; +// animation: linear infinite; +// animation-name: run; +// animation-duration: 5s; +// } + +.scrollTitle { + animation: scrollText 10s infinite linear; + animation-delay: 10s; +} + +@keyframes scrollText { + from { + transform: translateX(0%); + } + + to { + transform: translateX(-50%); + } +} + +@-webkit-keyframes run { + 0% { + right: 0; + } + 50% { + right: 100%; + } + 100% { + right: -2%; + } +} + +@keyframes run { + 0% { + right: 0; + } + 50% { + right: 100%; + } + 100% { + right: -2%; + } +} + +.recommendationsLabel { + color: rgb(227, 227, 227); + font-size: 32px; + font-weight: 510; + margin-bottom: 10px; +} + +.recommendationHeader { + display: flex; + align-items: center; +} + +.recommendationTarget { + position: relative; + color: white; + font-weight: 600; + font-size: 20px; + text-align: center; + align-items: center; + height: auto; + background-color: rgb(81, 126, 97); + padding-left: 10px; + padding-right: 10px; + padding-bottom: 9px; + padding-top: 5px; + border-radius: 10px; + margin-top: 7px; +} + +.recommendationsHeaderSpacer { + flex-grow: 1; + visibility: hidden; +} + +.settingContainer { + display: flex; + flex-direction: row; + padding-left: 20px; + padding-right: 30px; + align-items: center; +} + +.settingLabel { + font-size: larger; + font-weight: 500; + color: white; } \ No newline at end of file