diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml new file mode 100644 index 0000000..1b0ce81 --- /dev/null +++ b/.github/workflows/github-actions.yml @@ -0,0 +1,41 @@ +name: NodeJS + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Spicetify + continue-on-error: true + run: | + curl -fsSL https://raw.githubusercontent.com/spicetify/cli/main/install.sh | sh + + - name: Update path + run: | + echo "/home/runner/.spicetify" >> $GITHUB_PATH + + - name: npm install and build + run: | + echo $GITHUB_PATH + echo $PATH + npm install + npm run build diff --git a/package-lock.json b/package-lock.json index 1aace62..d60a29f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,17 @@ "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", + "@types/react-modal": "^3.16.3", + "jquery": "^3.7.1", + "react-circular-progressbar": "^2.1.0", + "react-modal": "^3.16.1" }, "devDependencies": { "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", + "@types/react-modal": "^3.16.3", "spicetify-creator": "^1.0.17" } }, @@ -44,17 +50,24 @@ "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", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/react": { "version": "18.3.4", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", - "dev": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -69,6 +82,21 @@ "@types/react": "*" } }, + "node_modules/@types/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==", + "dev": true, + "dependencies": { + "@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", @@ -338,8 +366,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/cwd": { "version": "0.10.0", @@ -784,6 +811,11 @@ "node": ">=6" } }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -1186,6 +1218,18 @@ "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" + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -1263,6 +1307,18 @@ "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", + "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", @@ -1376,6 +1432,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1580,6 +1644,16 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -1587,6 +1661,69 @@ "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/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1690,6 +1827,15 @@ "dev": true, "optional": true }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -1912,6 +2058,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -1950,17 +2104,23 @@ "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", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "@types/react": { "version": "18.3.4", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", - "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1975,6 +2135,20 @@ "@types/react": "*" } }, + "@types/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==", + "dev": true, + "requires": { + "@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", @@ -2137,8 +2311,7 @@ "csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "cwd": { "version": "0.10.0", @@ -2373,6 +2546,11 @@ "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -2673,6 +2851,16 @@ "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==" + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -2731,6 +2919,14 @@ "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==", + "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", @@ -2805,6 +3001,11 @@ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2942,6 +3143,16 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -2949,6 +3160,52 @@ "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": {} + }, + "react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "requires": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3030,6 +3287,15 @@ "dev": true, "optional": true }, + "scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, "semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -3180,6 +3446,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index fbf46e0..5317cf7 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,15 @@ "devDependencies": { "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", + "@types/react-modal": "^3.16.3", "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", + "@types/react-modal": "^3.16.3", + "jquery": "^3.7.1", + "react-circular-progressbar": "^2.1.0", + "react-modal": "^3.16.1" } } diff --git a/src/app.tsx b/src/app.tsx index 94303ea..6a752d4 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,13 +1,11 @@ import React from "react"; import NowPlaying from "./components/NowPlaying"; -import DynamicRecommendations from "./components/DynamicRecommendations"; class App extends React.Component<{}, {}> { render() { return ( <> - ); } diff --git a/src/components/DynamicRecommendations.tsx b/src/components/DynamicRecommendations.tsx index fa7c469..8b4c829 100644 --- a/src/components/DynamicRecommendations.tsx +++ b/src/components/DynamicRecommendations.tsx @@ -2,28 +2,46 @@ import styles from "../css/app.module.scss"; 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"; +import { RecommendationsRender } from "../services/enhancifyInternalService"; -class DynamicRecommendations extends React.Component<{}, {queue: Array, recommendations: GetRecommendationsResponse | {}}> { +class DynamicRecommendations extends React.Component<{recTargetProp : string}, {songQueue: Array, artistQueue: Array, recTarget: string, recommendations: GetRecommendationsResponse | {}}> { + state = { - queue: Spicetify.LocalStorage.get("queue")?.split(',') || new Array, - 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(); - apiOptions.data.seed_tracks = this.state.queue.toString(); + if (this.state.recTarget == "songs") { + apiOptions.data.seed_tracks = this.state.songQueue.toString(); + } + else if (this.state.recTarget == "artists") { + apiOptions.data.seed_artists = this.state.artistQueue.toString(); + } + + // Make the API call var recommendations = await getRecommendations(apiOptions); this.setState({ recommendations: recommendations, }); }; - + + // Add the current song and artist to the queue when the song is played half-way through addToQueue = (event?: Event & {data: number}) => { - if (!event || !Spicetify.Player.data || this.state.queue.includes(Spicetify.Player.data.item.uri.split(":")[2])) { + if (!event || !Spicetify.Player.data) { return; } @@ -32,31 +50,113 @@ class DynamicRecommendations extends React.Component<{}, {queue: Array, return; } - let newQueue = this.state.queue.slice(); - if (this.state.queue.length == 5) { + // Update the song and artist queue + this.setSongQueue(); + this.setArtistQueue(); + }; + + // Add songs to the song queue + setSongQueue = () => { + let curSongID = getID(Spicetify.Player.data.item.uri); + if (this.state.songQueue && this.state.songQueue[this.state.songQueue.length-1] == curSongID) { + return; + } + + let newQueue = this.state.songQueue.slice(); + if (newQueue.includes(curSongID)) { + newQueue = newQueue.filter((val, ind) => val != curSongID); + } + + newQueue.push(curSongID); + if (newQueue.length > 5) { newQueue.shift(); } - newQueue.push(Spicetify.Player.data.item.uri.split(":")[2]); this.setState({ - queue: newQueue, + songQueue: newQueue, }, () => { - if (Spicetify.LocalStorage.get("queue") == this.state.queue.toString()) { + if (Spicetify.LocalStorage.get("songQueue") == this.state.songQueue.toString()) { return; } - Spicetify.LocalStorage.set("queue", this.state.queue.toString()); - this.generateRecommendations(); + Spicetify.LocalStorage.set("songQueue", this.state.songQueue.toString()); + if (this.state.recTarget == "songs") { + this.generateRecommendations(); + } + }); + }; + + + shouldArtistQueueBeUpdated = (): boolean => { + if (!Spicetify.Player.data.item.artists) { + return false; + } + if (!this.state.artistQueue) { + return true; + } + + for (const artist of Spicetify.Player.data.item.artists) { + let fromIndex = Math.max(0, this.state.artistQueue.length - Spicetify.Player.data.item.artists.length); + if (!this.state.artistQueue.includes(getID(artist.uri), fromIndex)) { + return true; + }; + } + + return false; + }; + + // Add artists to the queue + setArtistQueue = () => { + if (!Spicetify.Player.data.item.artists || !this.shouldArtistQueueBeUpdated()) { + return; + } + + let newArtistQueue = this.state.artistQueue.slice(); + for (const artist of Spicetify.Player.data.item.artists) { + let artistID = getID(artist.uri); + if (newArtistQueue.includes(artistID)) { + newArtistQueue = newArtistQueue.filter((val, ind) => val != artistID); + } + newArtistQueue.push(artistID); + } + + while (newArtistQueue.length > 5) { + newArtistQueue.shift(); + } + + this.setState({ + artistQueue: newArtistQueue, + }, () => { + if (Spicetify.LocalStorage.get("artistQueue") == this.state.artistQueue.toString()) { + return; + } + Spicetify.LocalStorage.set("artistQueue", this.state.artistQueue.toString()); + if (this.state.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(); + } } render() { Spicetify.Player.addEventListener("onprogress", this.addToQueue); return ( <> - - {String(this.state.queue)} - {JSON.stringify(Object.keys(this.state.recommendations).length != 0 ? (this.state.recommendations as GetRecommendationsResponse)["tracks"][0].name : {})} - +
+
+
{"Song Recommendations"}
+
+
{this.props.recTargetProp}
+
+
+ {RecommendationsRender(this.state.recommendations)} +
+
); } diff --git a/src/components/NowPlaying.tsx b/src/components/NowPlaying.tsx index 86b0ab7..a0d5d3f 100644 --- a/src/components/NowPlaying.tsx +++ b/src/components/NowPlaying.tsx @@ -2,11 +2,36 @@ import styles from "../css/app.module.scss"; import React from "react"; import getAudioFeatures from "../services/nowPlayingService"; import { AudioFeaturesResponse } from "../types/spotify-web-api"; +import DynamicRecommendations from "./DynamicRecommendations"; +import SongMetric from "./SongMetric"; +import { SelectedMetrics, SongMetricData } from "../types/enhancify"; +import { allMetrics, getSongMetrics } from "../services/enhancifyInternalService"; +import RecommendationsModal from "./RecommendationsModal"; +import SettingsModal from "./SettingsModal"; +import Modal from 'react-modal'; -class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesResponse | {}, songURI: string}> { +class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesResponse | {}, + songURI: string, + recTarget: string, + songMetrics: SongMetricData[], + metricsToDisplay: string[], + modalIsOpen: boolean, + settingsModalIsOpen: boolean, + selectedMetrics: SelectedMetrics}> { + state = { - audioFeatures: {}, - songURI: "", + 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 + songMetrics: [], // Current song metric information + metricsToDisplay: Spicetify.LocalStorage.get("metricsToDisplay") != "" ? + Spicetify.LocalStorage.get("metricsToDisplay")?.split(',') || + ["Danceability", "Energy", "Acousticness", "Loudness", "Key", "Tempo"] : + [], // Current metric information types + modalIsOpen: false, // Whether the modal is currently open + settingsModalIsOpen: false, + selectedMetrics: JSON.parse(Spicetify.LocalStorage.get("selectedMetrics") || + "{}"), // Metrics that have been selected to be fed into the Spotify recommendations endpoint } componentDidMount = () => { @@ -14,31 +39,250 @@ class NowPlaying extends React.Component<{}, {audioFeatures: AudioFeaturesRespon } setAudioFeatures = () => { + // 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 () => { - var currentAudioFeatures = await getAudioFeatures(this.state.songURI || ""); + const currentAudioFeatures = await getAudioFeatures(this.state.songURI || ""); this.setState({ audioFeatures: currentAudioFeatures, - }); + }, this.setSongMetrics); } + // Make the API call apiCall(); } - + + // Sets the song metric information based on the type of information that the user wants to be displayed + setSongMetrics = () => { + this.setState({ + songMetrics: getSongMetrics((this.state.audioFeatures as AudioFeaturesResponse), this.state.metricsToDisplay) + }); + } + + // Change the recommendation target + changeRecTarget = () => { + if (this.state.recTarget == "songs") { + this.setState({ + recTarget: "artists", + }); + } + else if (this.state.recTarget == "artists") { + this.setState({ + recTarget: "songs", + }); + } + }; + + // Toggles whether the metric that the user clicked on should be displayed or not + toggleMetric = (metric: string) => { + let newArray = this.state.metricsToDisplay.slice(); + if (newArray.includes(metric)) { + newArray = newArray.filter((val) => val != metric); + + // If a metric is being hidden from the display, it should not be fed into the recommendations endpoint + if (metric in this.state.selectedMetrics) { + let copy: SelectedMetrics = { ...this.state.selectedMetrics }; + delete copy[metric]; + Spicetify.LocalStorage.set("selectedMetrics", JSON.stringify(copy)); + this.setState({ + selectedMetrics: copy + }); + } + } + else { + newArray.push(metric); + } + + Spicetify.LocalStorage.set("metricsToDisplay", newArray.toString()); + + this.setState({ + metricsToDisplay: newArray + }, this.setSongMetrics); + } + + // Set whether the modal should be open or closed + setModalIsOpen = (value: boolean) => { + this.setState({ + modalIsOpen: value + }); + } + + setSettingsModalIsOpen = (value: boolean) => { + this.setState({ + settingsModalIsOpen: value, + }); + } + + // Select a metric to toggle whether they should be included in the recommendations endpoint request or not + selectMetric = (metric: string, value: string) => { + let copy: SelectedMetrics = { ...this.state.selectedMetrics }; + if (metric in copy) { + delete copy[metric]; + } + else { + copy[metric] = value; + } + Spicetify.LocalStorage.set("selectedMetrics", JSON.stringify(copy)); + this.setState({ + selectedMetrics: copy + }); + } + + modalStyles = { + overlay: { + backgroundColor: "rgba(0, 0, 0, 0.70)", + }, + content: { + position: 'relative', + top: '60px', + left: '27.5%', + width: "550px", + height: "610px", + }, + } + + recommendationsModalStyles = { + overlay: { + backgroundColor: "rgba(0, 0, 0, 0.70)", + }, + content: { + position: 'relative', + top: '55px', + left: '21.5%', + width: "800px", + height: "600px" + }, + } + render() { + Spicetify.Player.addEventListener("songchange", this.setAudioFeatures); + return ( <> - - {JSON.stringify(this.state.audioFeatures)} - +
+
+ {Spicetify.Player.data ? +
+ {/* Track cover */} + {Spicetify.Player.data.item.images ? + Spicetify.Player.data.item.images.length > 0 ? + + : <> + : <>} + + {/* Track title */} + + {Spicetify.Player.data.item.name} + + + {/* Track artist(s) */} + {(function () { + + // Get all the artists + const trackArtists = Spicetify.Player.data.item.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 <>; + } + })()} + + {/* Track album */} + + {Spicetify.Player.data.item.album.name} + +
+ : <>} +
+
+ +
+
+ + {/* Stats block */} +
+
+ {"Song Statistics"} +
+
this.setState({modalIsOpen: true})}> + +
+
this.setSettingsModalIsOpen(true)}> + +
+
+
+ + {/* Stats block data */} + {this.state.songMetrics.map((songMetric: SongMetricData, i) => { + return ; + })} + +
+
+
+ this.setModalIsOpen(false)} style={this.recommendationsModalStyles}> + + + this.setSettingsModalIsOpen(false)} style={this.modalStyles}> + + ); } } -export default NowPlaying; +export default NowPlaying; \ No newline at end of file diff --git a/src/components/RecommendationsModal.tsx b/src/components/RecommendationsModal.tsx new file mode 100644 index 0000000..cdca288 --- /dev/null +++ b/src/components/RecommendationsModal.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import styles from "./../css/app.module.scss"; +import { GetRecommendationsInput, GetRecommendationsResponse, RecommendationsInput } from "../types/spotify-web-api.d"; +import getRecommendations from "../services/dynamicRecommendationsService"; +import { SelectedMetrics } from "../types/enhancify"; +import getID from './../services/common'; +import { RecommendationsRender } from "../services/enhancifyInternalService"; + +class RecommendationsModal extends React.Component<{setModalIsOpen: (value: boolean) => void, + songURI: string, + selectedMetrics: SelectedMetrics}, + {recommendations: GetRecommendationsResponse | {}}> { + + state = { + recommendations: {} // Recommendations that show up in the modal view + }; + + 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(); + apiOptions.data.seed_tracks = getID(this.props.songURI); + apiOptions.data.limit = "8"; + + for (let key in this.props.selectedMetrics) { + let apiDataKey = "target_" + key.toLowerCase(); + apiOptions.data[apiDataKey as keyof RecommendationsInput] = this.props.selectedMetrics[key]; + } + + // Make the API call + var recommendations = await getRecommendations(apiOptions); + this.setState({ + recommendations: recommendations, + }); + }; + + render() { + return ( +
+
+
+ {"More Recommendations"} +
+ this.props.setModalIsOpen(false)}/> +
+
+ {Object.keys(this.props.selectedMetrics).map((metric_name) =>
+ {metric_name} +
)} +
+
+ {RecommendationsRender(this.state.recommendations)} +
+
+ ); + } +} + +export default RecommendationsModal; diff --git a/src/components/RecommendedTrack.tsx b/src/components/RecommendedTrack.tsx new file mode 100644 index 0000000..73fa58a --- /dev/null +++ b/src/components/RecommendedTrack.tsx @@ -0,0 +1,70 @@ +import styles from "../css/app.module.scss"; +import React from "react"; + +class RecommendedTrack extends React.Component<{songCover: string, + songName: string, + songArtists: string[], + songAlbum: string, + songURI: string}, + {paddingRight: string}> { + + // Plays the recommended song whose play icon has been clicked + playSong = () => { + Spicetify.Player.playUri(this.props.songURI); + } + + 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/components/SettingsModal.tsx b/src/components/SettingsModal.tsx new file mode 100644 index 0000000..aed13d1 --- /dev/null +++ b/src/components/SettingsModal.tsx @@ -0,0 +1,51 @@ +import styles from "../css/app.module.scss"; +import React from "react"; +import { allMetrics, getSongMetrics } from "../services/enhancifyInternalService"; + +class SettingsModal extends React.Component<{changeRecTarget: () => void, + toggleMetric: (metric: string) => void, + setModalIsOpen: (value: boolean) => void, + recTarget: string, + metricsToDisplay: string[]} , {}> { + + render() { + return ( +
+
+
+ {"Settings"} +
+ this.props.setModalIsOpen(false)}/> +
+
+ {"Show recommendations by: "} + +
+
+ {"Displayed statistics: "} +
+
+ {allMetrics.map((metric: string, i) => { + return (); + })} +
+
); + } +} + +export default SettingsModal; \ No newline at end of file diff --git a/src/components/SongMetric.tsx b/src/components/SongMetric.tsx new file mode 100644 index 0000000..15356a9 --- /dev/null +++ b/src/components/SongMetric.tsx @@ -0,0 +1,57 @@ +import styles from "../css/app.module.scss"; +import React from "react"; +import { CircularProgressbar } from 'react-circular-progressbar'; +import 'react-circular-progressbar/dist/styles.css'; + +// Component for each individual song metric data block +class SongMetric extends React.Component<{floatValue: string, + title: string, + progressBar: boolean, + label: string, + selectMetric: (metric: string, value: string) => void + isMetricSelected: boolean}, + {}> { + + render() { + return ( +
this.props.selectMetric(this.props.title, this.props.floatValue)} + style={this.props.isMetricSelected ? {backgroundColor: "rgb(99, 155, 119", + border: "2px solid white", + } : {}}> +
+
+ {this.props.title} +
+
+ {Math.round(parseFloat(this.props.floatValue) * (this.props.progressBar ? 100 : 1))} + + {/* Label represents the unit (something like dB, bpm, etc.) */} + { this.props.label != "" ? + + {this.props.label} + : <> } +
+
+ { this.props.progressBar ? +
+ + +
: + <> } +
+ ); + } +} + +export default SongMetric; \ No newline at end of file diff --git a/src/css/app.module.scss b/src/css/app.module.scss index 9c0eb39..077e693 100644 --- a/src/css/app.module.scss +++ b/src/css/app.module.scss @@ -2,3 +2,327 @@ font-size: 16px; word-wrap: break-word; } + +.topBar { + display: flex; + flex-direction: row; +} + +.nowPlayingSidebar { + width: 300px; + display: flex; + flex-direction: column; + align-items: center; + height:auto; +} + +.trackInfoPrimary { + width: 280px; + margin-top: 35px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.trackCover { + border-radius: 5px; + width: 250px; + height: 250px; +} + +.statsBlock { + width: 105%; + margin-top: 40px; + margin: 20px; + margin-right: 0px; + margin-bottom: 0px; + margin-left: 0px; + border-radius: 5px; +} + +.statsBlock { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding: 10px; + padding-right: 0px; + padding-top: 0px; +} + +.statContainer { + background-color: rgb(81, 126, 97); + width: 30%; + height: 145px; + border-radius: 9px; + margin: 5px; + padding: 10px; + padding-left: 30px; + padding-right: 30px; + padding-bottom: 10px; + display: flex; +} + +.statContainer:hover { + background-color: rgb(99, 155, 119); +} + +.statTitle { + font-weight: bolder; + font-size: larger; +} + +.statLabel { + font-weight: 600; + width: 100px; + text-overflow: ellipsis; +} + +.statValue { + margin-top: 10px; + font-weight: 600; +} + +.graphicContainer { + justify-content: flex-end; + align-self: center; + margin-left: auto; + width: 100px; + 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: 13px; + column-gap: 10px; +} + +.trackContainer { + display: flex; + background-image: linear-gradient(to right, rgb(43, 43, 43), rgb(79, 79, 79)); + border-radius: 10px; + padding: 10px; + padding-right: 20px; +} + +.trackContainer:hover { + background-image: linear-gradient(to right, rgb(62, 62, 62), rgb(99, 99, 99)); +} + +.recommendationsCover { + width: 70px; + height: 70px; + border-radius: 6px; + margin-right: 20px; +} + +.trackName { + width: 150px; + overflow: hidden; + white-space: nowrap; +} + +.playIcon { + width: 40px; + height: 40px; + margin-left: auto; + align-self: center; + filter: brightness(90%); +} + +.playIcon:hover { + cursor: pointer; + filter: brightness(100%); + width: 41px; + height: 41px; +} + +.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 { + 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: 20px; + margin-bottom: 10px; + align-items: center; + flex-wrap: wrap; +} + +.settingLabel { + font-size: larger; + font-weight: 500; + color: white; +} + +.modal { + background-color: black; + height: 80%; + width: 80%; + margin-right: 50px; + margin-top: 50px; + margin-left: 50px; + margin-bottom: 50px; + text-wrap: wrap; +} + +.settingsModalContainer { + padding: 20px; + display: flex; + flex-direction: column; +} + +.recommendationsModalContainer { + padding: 40px; + padding-top: 20px; + padding-bottom: 20px; +} + +.modalHeaderContainer { + display: flex; + flex-direction: row; + margin-bottom: 20px; +} + +.sectionHeaderContainer { + display: flex; + align-items: center; +} + +.settingsIconContainer:hover { + background-color: rgb(0, 0, 0); + cursor: pointer; +} + +.settingsIconContainer { + margin-left: 10px; + margin-right: 45px; + width: 50px; + height: 50px; + display: flex; + justify-content: center; + background-color: rgb(43, 43, 43); + border-radius: 7px; +} + +.metricsRecommendationContainer { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(4, 1fr); + row-gap: 13px; + column-gap: 10px; +} \ No newline at end of file diff --git a/src/services/common.tsx b/src/services/common.tsx new file mode 100644 index 0000000..ba31080 --- /dev/null +++ b/src/services/common.tsx @@ -0,0 +1,5 @@ +function getID(uri: string): string { + return uri.split(":")[2]; +} + +export default getID; diff --git a/src/services/enhancifyInternalService.tsx b/src/services/enhancifyInternalService.tsx new file mode 100644 index 0000000..d2b3ef1 --- /dev/null +++ b/src/services/enhancifyInternalService.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import RecommendedTrack from "../components/RecommendedTrack"; +import { Labels, MetricFeatures, SongMetricData } from "../types/enhancify"; +import { AudioFeaturesResponse, GetRecommendationsResponse } from "../types/spotify-web-api"; + +// Creates the recommended track view for any response from the Spotify recommendations endpoint +export function RecommendationsRender(recommendations : GetRecommendationsResponse | {}) { + if (Object.keys(recommendations).length == 0) { + return; + } + let recs = (recommendations as GetRecommendationsResponse)["tracks"]; + let recommendedTracksHTML = []; + for (let i = 0; i < recs.length; i++) { + let recommendedSong = artist.name)} + songURI={recs[i].uri} + key={i}> + ; + recommendedTracksHTML.push(recommendedSong); + } + return recommendedTracksHTML; +} + +// Dynamically fills in the song metric information based on the specific metrics that the user wants to display +export function getSongMetrics(audioFeatures: AudioFeaturesResponse, metricsToDisplay: string[]): SongMetricData[] { + let res: SongMetricData[] = []; + for (const metric of metricsToDisplay) { + res.push({ + title: metric, + floatValue: audioFeatures[metric.toLowerCase() as keyof AudioFeaturesResponse], + label: metric in metricFeatures.label ? metricFeatures.label[metric as keyof Labels] : "", + progressBar: metricFeatures.progressbar.has(metric), + }); + } + return res; +} + +// Object that represents which metrics require a progress bar and which metrics require a specific label +const metricFeatures: MetricFeatures = { + progressbar: new Set(["Danceability", "Energy", "Acousticness", "Instrumentalness", "Speechiness", "Valence", "Liveness"]), + label: { + Loudness: "dB", + Tempo: "bpm", + Time_Signature: "/4", + Key: "in Pitch Class", + Mode: "(0: Minor, 1: Major)", + } +}; + +// Array of all the metric types that we allow to be shown +export const allMetrics: string[] = ["Danceability", "Energy", "Acousticness", "Loudness", "Key", "Tempo", "Instrumentalness", "Liveness", "Mode", "Speechiness", "Time_Signature", "Valence"]; diff --git a/src/services/nowPlayingService.tsx b/src/services/nowPlayingService.tsx index f7c3c84..20f9f00 100644 --- a/src/services/nowPlayingService.tsx +++ b/src/services/nowPlayingService.tsx @@ -1,4 +1,5 @@ import type { AudioFeaturesResponse } from "../types/spotify-web-api"; +import getID from "./common"; async function getAudioFeatures(songURI: string | undefined): Promise { if (!songURI) { @@ -7,7 +8,7 @@ async function getAudioFeatures(songURI: string | undefined): Promise, + label: Labels +}; + +export type SelectedMetrics = { + [metric: string]: string +}; diff --git a/src/types/spotify-web-api.d.ts b/src/types/spotify-web-api.d.ts index a312d87..4e33947 100644 --- a/src/types/spotify-web-api.d.ts +++ b/src/types/spotify-web-api.d.ts @@ -20,59 +20,61 @@ export type AudioFeaturesResponse = { }; export class GetRecommendationsInput { - data = { - limit: "1", - market: "", - seed_artists: "", - seed_genres: "", - seed_tracks: "", - min_acousticness: "", - max_acousticness: "", - target_acousticness: "", - min_danceability: "", - max_danceability: "", - target_danceability: "", - min_duration_ms: "", - max_duration_ms: "", - target_duration_ms: "", - min_energy: "", - max_energy: "", - target_energy: "", - min_instrumentalness: "", - max_instrumentalness: "", - target_instrumentalness: "", - min_key: "", - max_key: "", - target_key: "", - min_liveness: "", - max_liveness: "", - target_liveness: "", - min_loudness: "", - max_loudness: "", - target_loudness: "", - min_mode: "", - max_mode: "", - target_mode: "", - min_popularity: "", - max_popularity: "", - target_popularity: "", - min_speechiness: "", - max_speechiness: "", - target_speechiness: "", - min_tempo: "", - max_tempo: "", - target_tempo: "", - min_time_signature: "", - max_time_signature: "", - target_time_signature: "", - min_valence: "", - max_valence: "", - target_valence: "", - }; + data = new RecommendationsInput(); +}; + +export class RecommendationsInput { + limit = "6"; + market = ""; + seed_artists = ""; + seed_genres = ""; + seed_tracks = ""; + min_acousticness = ""; + max_acousticness = ""; + target_acousticness = ""; + min_danceability = ""; + max_danceability = ""; + target_danceability = ""; + min_duration_ms = ""; + max_duration_ms = ""; + target_duration_ms = ""; + min_energy = ""; + max_energy = ""; + target_energy = ""; + min_instrumentalness = ""; + max_instrumentalness = ""; + target_instrumentalness = ""; + min_key = ""; + max_key = ""; + target_key = ""; + min_liveness = ""; + max_liveness = ""; + target_liveness = ""; + min_loudness = ""; + max_loudness = ""; + target_loudness = ""; + min_mode = ""; + max_mode = ""; + target_mode = ""; + min_popularity = ""; + max_popularity = ""; + target_popularity = ""; + min_speechiness = ""; + max_speechiness = ""; + target_speechiness = ""; + min_tempo = ""; + max_tempo = ""; + target_tempo = ""; + min_time_signature = ""; + max_time_signature = ""; + target_time_signature = ""; + min_valence = ""; + max_valence = ""; + target_valence = ""; }; export type GetRecommendationsResponse = { - seeds: [ + seeds: { afterFilteringSize: number, afterRelinkingSize: number, @@ -80,9 +82,8 @@ export type GetRecommendationsResponse = { id: string, initialPoolSize: number, type: string - } - ], - tracks: [ + }[], + tracks: { album: { album_type: string, @@ -93,13 +94,12 @@ export type GetRecommendationsResponse = { }, href: string, id: string, - images: [ + images: { url: string, height: number, width: number, - } - ], + }[], name: string, release_date: string, release_date_precision: string, @@ -108,7 +108,7 @@ export type GetRecommendationsResponse = { }, type: string, uri: string, - artists: [ + artists: { external_urls: { spotify: string @@ -118,10 +118,9 @@ export type GetRecommendationsResponse = { name: string, type: string, uri: string - } - ] + }[] }, - artists: [ + artists: { external_urls: { spotify: string @@ -131,11 +130,9 @@ export type GetRecommendationsResponse = { name: string, type: string, uri: string - } - ], - available_markets: [ - string - ], + }[], + available_markets: + string[], disc_number: number, duration_ms: number, explicit: boolean, @@ -161,6 +158,5 @@ export type GetRecommendationsResponse = { type: string, uri: string, is_local: boolean - } - ] -} + }[] +};