diff --git a/.eslintrc.js b/.eslintrc.js index 5518fcd8..8a482da6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,70 +1,39 @@ -// eslint-disable-next-line no-undef module.exports = { - "ignorePatterns": ["node_modules", "dist"], - "env": { - "browser": true, - "es2021": true, + ignorePatterns: ["node_modules", "dist", ".eslintrc.js", "spicetify.d.ts"], + env: { + browser: true, + es2022: true, }, - "extends": [ + extends: [ "eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended", ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true, - }, - "ecmaVersion": "latest", - "sourceType": "module", + parser: "@typescript-eslint/parser", + parserOptions: { + sourceType: "module", + project: ["tsconfig.json"], }, - "plugins": [ - "react", - "@typescript-eslint", - ], - "rules": { - "indent": [ - "error", - 2, - ], - "linebreak-style": [ - "error", - "unix", - ], - "quotes": [ - "error", - "double", - { "allowTemplateLiterals": true }, - ], - "semi": [ - "error", - "always", - ], - "comma-dangle": [ - "error", - "always-multiline", - ], + plugins: ["react", "@typescript-eslint"], + rules: { + indent: ["error", 2], + "linebreak-style": ["error", "unix"], + quotes: ["error", "double", { allowTemplateLiterals: true }], + semi: ["error", "always"], + "comma-dangle": ["error", "always-multiline"], "no-var": "error", "space-before-blocks": "error", - "comma-spacing": [ - "error", { "before": false, "after": true }, - ], + "comma-spacing": ["error", { before: false, after: true }], "no-trailing-spaces": "error", "keyword-spacing": "error", - "no-multiple-empty-lines": [ - "error", { "max": 1 }, - ], - "object-curly-spacing": [ - "error", "always", - ], - "key-spacing": [ - "error", { "beforeColon": false, "afterColon": true }, - ], + "no-multiple-empty-lines": ["error", { max: 1 }], + "object-curly-spacing": ["error", "always"], + "key-spacing": ["error", { beforeColon: false, afterColon: true }], }, - "settings": { - "react": { + settings: { + react: { // This is what Spotify uses - "version": "17", + version: "17.0.2", }, }, }; diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 38c3e2ec..a6812aee 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,4 +2,7 @@ blank_issues_enabled: false contact_links: - name: ✏️ Spicetify Docs url: https://spicetify.app/ - about: Check out our documentation here. + about: Read our documentation before creating an issue. + - name: ⛑️ Spicetify Discord Server + url: https://discord.gg/spicetify-842219447716151306 + about: Issues are not for support. If you need it - join our Discord server. diff --git a/package.json b/package.json index 9d0160a7..28d9cad5 100644 --- a/package.json +++ b/package.json @@ -1,55 +1,53 @@ { - "name": "spicetify-marketplace", - "version": "0.9.2", - "homepage": "https://github.com/spicetify/spicetify-marketplace", - "repository": { - "type": "git", - "url": "git@github.com:spicetify/spicetify-marketplace.git" - }, - "bugs": { - "url": "https://github.com/spicetify/spicetify-marketplace/issues" - }, - "packageManager": "pnpm@8.10.2", - "scripts": { - "build": "spicetify-creator", - "build:local": "spicetify-creator --out=dist --minify", - "build:prod": "pnpm build:local && pnpm copy:docs", - "copy:docs": "copyfiles README.md dist/", - "lint": "eslint --fix src", - "lint:ci": "eslint src", - "type-check": "tsc --noEmit", - "watch": "spicetify-creator --watch", - "prepare": "husky install" - }, - "engines": { - "yarn": "please-use-pnpm", - "npm": "please-use-pnpm", - "pnpm": ">=8", - "node": ">=20" - }, - "devDependencies": { - "@types/chroma-js": "^2.4.2", - "@types/react": "^17.0.65", - "@types/react-dom": "^17.0.20", - "@types/semver": "^7.5.4", - "@typescript-eslint/eslint-plugin": "^6.9.1", - "@typescript-eslint/parser": "^6.9.1", - "chroma-js": "^2.4.2", - "copyfiles": "^2.4.1", - "eslint": "^8.53.0", - "eslint-plugin-react": "^7.33.2", - "husky": "^8.0.3", - "i18next": "^23.6.0", - "i18next-browser-languagedetector": "^7.1.0", - "prismjs": "^1.29.0", - "react-dropdown": "^1.11.0", - "react-i18next": "^13.3.1", - "react-simple-code-editor": "^0.13.1", - "semver": "^7.5.4", - "spcr-whats-new": "^1.0.1", - "spicetify-creator": "^1.0.16", - "typescript": "^5.2.2", - "util": "^0.12.5" - }, - "private": true + "name": "spicetify-marketplace", + "version": "0.9.3", + "homepage": "https://github.com/spicetify/spicetify-marketplace", + "repository": "github:spicetify/spicetify-marketplace", + "bugs": { + "url": "https://github.com/spicetify/spicetify-marketplace/issues" + }, + "packageManager": "pnpm@8.10.5", + "scripts": { + "build": "spicetify-creator", + "build:local": "spicetify-creator --out=dist --minify", + "build:prod": "pnpm build:local && pnpm copy:docs", + "copy:docs": "copyfiles README.md dist/", + "lint": "eslint --fix src", + "lint:ci": "eslint src", + "type-check": "tsc --noEmit", + "watch": "spicetify-creator --watch", + "prepare": "husky install", + "update-types": "curl -s -o src/types/spicetify.d.ts https://raw.githubusercontent.com/spicetify/spicetify-cli/master/globals.d.ts" + }, + "engines": { + "yarn": "please-use-pnpm", + "npm": "please-use-pnpm", + "pnpm": ">=8", + "node": ">=20" + }, + "devDependencies": { + "@types/chroma-js": "^2.4.3", + "@types/react": "17.0.2", + "@types/react-dom": "17.0.2", + "@types/semver": "^7.5.5", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "copyfiles": "^2.4.1", + "eslint": "^8.53.0", + "eslint-plugin-react": "^7.33.2", + "husky": "^8.0.3", + "spicetify-creator": "^1.0.16", + "typescript": "^5.2.2" + }, + "dependencies": { + "react-dropdown": "^1.11.0", + "chroma-js": "^2.4.2", + "i18next": "^23.7.6", + "i18next-browser-languagedetector": "^7.2.0", + "prismjs": "^1.29.0", + "react-i18next": "^13.5.0", + "react-simple-code-editor": "^0.13.1", + "semver": "^7.5.4" + }, + "private": true } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b653b55c..c6d17e34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,28 +4,51 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -devDependencies: - '@types/chroma-js': +dependencies: + chroma-js: specifier: ^2.4.2 version: 2.4.2 + i18next: + specifier: ^23.7.6 + version: 23.7.6 + i18next-browser-languagedetector: + specifier: ^7.2.0 + version: 7.2.0 + prismjs: + specifier: ^1.29.0 + version: 1.29.0 + react-dropdown: + specifier: ^1.11.0 + version: 1.11.0(react-dom@18.2.0)(react@18.2.0) + react-i18next: + specifier: ^13.5.0 + version: 13.5.0(i18next@23.7.6)(react-dom@18.2.0)(react@18.2.0) + react-simple-code-editor: + specifier: ^0.13.1 + version: 0.13.1(react-dom@18.2.0)(react@18.2.0) + semver: + specifier: ^7.5.4 + version: 7.5.4 + +devDependencies: + '@types/chroma-js': + specifier: ^2.4.3 + version: 2.4.3 '@types/react': - specifier: ^17.0.65 - version: 17.0.65 + specifier: 17.0.2 + version: 17.0.2 '@types/react-dom': - specifier: ^17.0.20 - version: 17.0.20 + specifier: 17.0.2 + version: 17.0.2 '@types/semver': - specifier: ^7.5.4 - version: 7.5.4 + specifier: ^7.5.5 + version: 7.5.5 '@typescript-eslint/eslint-plugin': - specifier: ^6.9.1 - version: 6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.53.0)(typescript@5.2.2) + specifier: ^6.11.0 + version: 6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: ^6.9.1 - version: 6.9.1(eslint@8.53.0)(typescript@5.2.2) - chroma-js: - specifier: ^2.4.2 - version: 2.4.2 + specifier: ^6.11.0 + version: 6.11.0(eslint@8.53.0)(typescript@5.2.2) copyfiles: specifier: ^2.4.1 version: 2.4.1 @@ -38,39 +61,12 @@ devDependencies: husky: specifier: ^8.0.3 version: 8.0.3 - i18next: - specifier: ^23.6.0 - version: 23.6.0 - i18next-browser-languagedetector: - specifier: ^7.1.0 - version: 7.1.0 - prismjs: - specifier: ^1.29.0 - version: 1.29.0 - react-dropdown: - specifier: ^1.11.0 - version: 1.11.0(react-dom@18.2.0)(react@18.2.0) - react-i18next: - specifier: ^13.3.1 - version: 13.3.1(i18next@23.6.0)(react-dom@18.2.0)(react@18.2.0) - react-simple-code-editor: - specifier: ^0.13.1 - version: 0.13.1(react-dom@18.2.0)(react@18.2.0) - semver: - specifier: ^7.5.4 - version: 7.5.4 - spcr-whats-new: - specifier: ^1.0.1 - version: 1.0.1 spicetify-creator: specifier: ^1.0.16 version: 1.0.16(less@4.2.0)(postcss@8.4.31)(sass@1.69.3)(stylus@0.60.0) typescript: specifier: ^5.2.2 version: 5.2.2 - util: - specifier: ^0.12.5 - version: 0.12.5 packages: @@ -88,7 +84,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.0 - dev: true + dev: false /@esbuild/linux-loong64@0.14.54: resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} @@ -177,8 +173,8 @@ packages: fastq: 1.15.0 dev: true - /@types/chroma-js@2.4.2: - resolution: {integrity: sha512-gbiHvCuBS9aXkE3OEDfS69bscNLTYtbbx2TQf6WyOu+4eCH1AH1gPSiDGF2UzwkRFAbqKNsC5F0mY0xcaEHCbg==} + /@types/chroma-js@2.4.3: + resolution: {integrity: sha512-1ly5ly/7S/YF8aD7MxUQnFOZxdegimuOunJl0xDsLlguu5JrwuSTVGVH3UpIUlh6YauI0RMNT4cqjBonhgbdIQ==} dev: true /@types/json-schema@7.0.13: @@ -189,30 +185,25 @@ packages: resolution: {integrity: sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==} dev: true - /@types/react-dom@17.0.20: - resolution: {integrity: sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA==} + /@types/react-dom@17.0.2: + resolution: {integrity: sha512-Icd9KEgdnFfJs39KyRyr0jQ7EKhq8U6CcHRMGAS45fp5qgUvxL3ujUCfWFttUK2UErqZNj97t9gsVPNAqcwoCg==} dependencies: - '@types/react': 17.0.65 + '@types/react': 17.0.2 dev: true - /@types/react@17.0.65: - resolution: {integrity: sha512-oxur785xZYHvnI7TRS61dXbkIhDPnGfsXKv0cNXR/0ml4SipRIFpSMzA7HMEfOywFwJ5AOnPrXYTEiTRUQeGlQ==} + /@types/react@17.0.2: + resolution: {integrity: sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==} dependencies: '@types/prop-types': 15.7.8 - '@types/scheduler': 0.16.4 csstype: 3.1.2 dev: true - /@types/scheduler@0.16.4: - resolution: {integrity: sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==} + /@types/semver@7.5.5: + resolution: {integrity: sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==} dev: true - /@types/semver@7.5.4: - resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==} - dev: true - - /@typescript-eslint/eslint-plugin@6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==} + /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha @@ -223,11 +214,11 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.9.1 - '@typescript-eslint/parser': 6.9.1(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/scope-manager': 6.9.1 - '@typescript-eslint/type-utils': 6.9.1(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.9.1(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.11.0 debug: 4.3.4 eslint: 8.53.0 graphemer: 1.4.0 @@ -240,8 +231,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.9.1(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==} + /@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -250,10 +241,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.9.1 - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.11.0 debug: 4.3.4 eslint: 8.53.0 typescript: 5.2.2 @@ -261,16 +252,16 @@ packages: - supports-color dev: true - /@typescript-eslint/scope-manager@6.9.1: - resolution: {integrity: sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==} + /@typescript-eslint/scope-manager@6.11.0: + resolution: {integrity: sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/visitor-keys': 6.11.0 dev: true - /@typescript-eslint/type-utils@6.9.1(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==} + /@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -279,8 +270,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) - '@typescript-eslint/utils': 6.9.1(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2) + '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.2.2) debug: 4.3.4 eslint: 8.53.0 ts-api-utils: 1.0.3(typescript@5.2.2) @@ -289,13 +280,13 @@ packages: - supports-color dev: true - /@typescript-eslint/types@6.9.1: - resolution: {integrity: sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==} + /@typescript-eslint/types@6.11.0: + resolution: {integrity: sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.9.1(typescript@5.2.2): - resolution: {integrity: sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==} + /@typescript-eslint/typescript-estree@6.11.0(typescript@5.2.2): + resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -303,8 +294,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/visitor-keys': 6.9.1 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/visitor-keys': 6.11.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 @@ -315,18 +306,18 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.9.1(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==} + /@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) '@types/json-schema': 7.0.13 - '@types/semver': 7.5.4 - '@typescript-eslint/scope-manager': 6.9.1 - '@typescript-eslint/types': 6.9.1 - '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.2.2) eslint: 8.53.0 semver: 7.5.4 transitivePeerDependencies: @@ -334,11 +325,11 @@ packages: - typescript dev: true - /@typescript-eslint/visitor-keys@6.9.1: - resolution: {integrity: sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==} + /@typescript-eslint/visitor-keys@6.11.0: + resolution: {integrity: sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.9.1 + '@typescript-eslint/types': 6.11.0 eslint-visitor-keys: 3.4.3 dev: true @@ -566,11 +557,11 @@ packages: /chroma-js@2.4.2: resolution: {integrity: sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==} - dev: true + dev: false /classnames@2.3.2: resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} - dev: true + dev: false /clean-css@5.3.2: resolution: {integrity: sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==} @@ -1499,7 +1490,7 @@ packages: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} dependencies: void-elements: 3.1.0 - dev: true + dev: false /husky@8.0.3: resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} @@ -1507,17 +1498,17 @@ packages: hasBin: true dev: true - /i18next-browser-languagedetector@7.1.0: - resolution: {integrity: sha512-cr2k7u1XJJ4HTOjM9GyOMtbOA47RtUoWRAtt52z43r3AoMs2StYKyjS3URPhzHaf+mn10hY9dZWamga5WPQjhA==} + /i18next-browser-languagedetector@7.2.0: + resolution: {integrity: sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==} dependencies: '@babel/runtime': 7.23.2 - dev: true + dev: false - /i18next@23.6.0: - resolution: {integrity: sha512-z0Cxr0MGkt+kli306WS4nNNM++9cgt2b2VCMprY92j+AIab/oclgPxdwtTZVLP1zn5t5uo8M6uLsZmYrcjr3HA==} + /i18next@23.7.6: + resolution: {integrity: sha512-O66BhXBw0fH4bEJMA0/klQKPEbcwAp5wjXEL803pdAynNbg2f4qhLIYlNHJyE7icrL6XmSZKPYaaXwy11kJ6YQ==} dependencies: '@babel/runtime': 7.23.2 - dev: true + dev: false /iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} @@ -1595,14 +1586,6 @@ packages: side-channel: 1.0.4 dev: true - /is-arguments@1.1.1: - resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - dev: true - /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: @@ -1815,7 +1798,6 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} @@ -1927,14 +1909,12 @@ packages: hasBin: true dependencies: js-tokens: 4.0.0 - dev: true /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true /make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} @@ -2276,7 +2256,7 @@ packages: /prismjs@1.29.0: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} engines: {node: '>=6'} - dev: true + dev: false /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -2313,7 +2293,7 @@ packages: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: true + dev: false /react-dropdown@1.11.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-E2UWetRPxNdIhQahXw6b984ME7WmcgDj9AEAjrtS/oyLCFVo+2qkCfcS06C22JR0Zj+YLnygwv0Ozf6VKKDq7g==} @@ -2324,10 +2304,10 @@ packages: classnames: 2.3.2 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true + dev: false - /react-i18next@13.3.1(i18next@23.6.0)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-JAtYREK879JXaN9GdzfBI4yJeo/XyLeXWUsRABvYXiFUakhZJ40l+kaTo+i+A/3cKIED41kS/HAbZ5BzFtq/Og==} + /react-i18next@13.5.0(i18next@23.7.6)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==} peerDependencies: i18next: '>= 23.2.3' react: '>= 16.8.0' @@ -2341,10 +2321,10 @@ packages: dependencies: '@babel/runtime': 7.23.2 html-parse-stringify: 3.0.1 - i18next: 23.6.0 + i18next: 23.7.6 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true + dev: false /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2358,14 +2338,14 @@ packages: dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true + dev: false /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: true + dev: false /readable-stream@1.0.34: resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} @@ -2409,7 +2389,7 @@ packages: /regenerator-runtime@0.14.0: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} - dev: true + dev: false /regexp.prototype.flags@1.5.1: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} @@ -2539,7 +2519,7 @@ packages: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: loose-envify: 1.4.0 - dev: true + dev: false /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} @@ -2559,7 +2539,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /set-function-name@2.0.1: resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} @@ -2617,12 +2596,6 @@ packages: engines: {node: '>= 8'} dev: true - /spcr-whats-new@1.0.1: - resolution: {integrity: sha512-hLr3PvgVGVqRoDdJEfPxjdmOeJMz0uzjfELk2QBYQT0ulxPkUXEBqe7/scNTdi/Z1mwy1gIZNNjnpOORk1Xwug==} - dependencies: - semver: 7.5.4 - dev: true - /spicetify-creator@1.0.16(less@4.2.0)(postcss@8.4.31)(sass@1.69.3)(stylus@0.60.0): resolution: {integrity: sha512-b/DPC6qmVBhFFViSDVJe6qVgi/qpnhDg75UVb8aIkmEnSsm4AreI/dbUL10JLo+1sneaCIRvm3E117eLsRjEzA==} hasBin: true @@ -2888,20 +2861,10 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true - /util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - dependencies: - inherits: 2.0.4 - is-arguments: 1.1.1 - is-generator-function: 1.0.10 - is-typed-array: 1.1.12 - which-typed-array: 1.1.11 - dev: true - /void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} - dev: true + dev: false /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -2991,7 +2954,6 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} diff --git a/resources/assets/snippets/smaller-right-sidebar-cover.png b/resources/assets/snippets/smaller-right-sidebar-cover.png new file mode 100644 index 00000000..fec32411 Binary files /dev/null and b/resources/assets/snippets/smaller-right-sidebar-cover.png differ diff --git a/src/app.tsx b/src/app.tsx index 2af30a86..010fe72a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -42,21 +42,7 @@ i18n }, }); -// TODO: the mono-stylesheet doesn't seem to import nested component stylesheets properly on build? -// import './styles/styles.scss'; -import "./styles/components/_grid.scss"; -import "./styles/components/_dropdown.scss"; -import "./styles/components/_card.scss"; -import "./styles/components/_settings.scss"; -import "./styles/components/_reload-modal.scss"; -import "./styles/components/_add-snippet-modal.scss"; -import "./styles/components/_readme-pages.scss"; -import "./styles/components/_fixes.scss"; -import "./styles/components/prismjs-themes/prism-tomorrow.scss"; -import "./styles/components/_devtools.scss"; -import "./styles/components/_code-editors.scss"; -import "./styles/components/_backup.scss"; - +import "./styles/styles.scss"; import Grid from "./components/Grid"; import ReadmePage from "./components/ReadmePage"; import { getLocalStorageDataFromKey } from "./logic/Utils"; diff --git a/src/components/Grid.tsx b/src/components/Grid.tsx index 0bd5e9be..d2b36820 100644 --- a/src/components/Grid.tsx +++ b/src/components/Grid.tsx @@ -11,7 +11,7 @@ import { getLocalStorageDataFromKey, generateSortOptions, sortCardItems, } from "../logic/Utils"; -import { LOCALSTORAGE_KEYS, ITEMS_PER_REQUEST, MARKETPLACE_VERSION, LATEST_RELEASE } from "../constants"; +import { LOCALSTORAGE_KEYS, ITEMS_PER_REQUEST, MARKETPLACE_VERSION, LATEST_RELEASE_URL } from "../constants"; import { openModal } from "../logic/LaunchModals"; import { getTaggedRepos, @@ -27,7 +27,6 @@ import { TopBarContent } from "./TabBar"; import Card from "./Card/Card"; import Button from "./Button"; import DownloadIcon from "./Icons/DownloadIcon"; -import Changelog from "./Modals/Changelog"; class Grid extends React.Component< { @@ -457,11 +456,11 @@ class Grid extends React.Component< */ async componentDidMount() { // Checks for new Marketplace updates - fetch(LATEST_RELEASE).then(res => res.json()).then( + fetch(LATEST_RELEASE_URL).then(res => res.json()).then( result => { if (result.message) throw result; this.setState({ - version: result[0].name, + version: result.name, }); try { @@ -475,8 +474,6 @@ class Grid extends React.Component< }, ); - Changelog(); - this.gridUpdateTabs = this.updateTabs.bind(this); this.gridUpdatePostsVisual = this.updatePostsVisual.bind(this); @@ -541,7 +538,7 @@ class Grid extends React.Component<
{this.state.newUpdate ? +
diff --git a/src/components/Modals/Update/index.tsx b/src/components/Modals/Update/index.tsx new file mode 100644 index 00000000..ea1acd15 --- /dev/null +++ b/src/components/Modals/Update/index.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { t } from "i18next"; + +import { + LATEST_RELEASE_URL, + MARKETPLACE_VERSION, + RELEASES_URL, +} from "../../../constants"; +import { getMarkdownHTML } from "../../../logic/Utils"; + +async function fetchLatestReleaseInfo(): Promise<{ + version: string; + changelog: string | null; +} | null> { + try { + const result = await fetch(LATEST_RELEASE_URL); + const resultJson = await result.json(); + const { body, tag_name, message } = resultJson; + return body && tag_name && !message + ? { + version: tag_name.replace("v", ""), + changelog: await getMarkdownHTML( + body.match(/## What's Changed([\s\S]*?)\r\n\r/)[1], + "spicetify", + "spicetify-marketplace", + ), + } + : null; + } catch (error) { + console.error(error); + return null; + } +} + +function UpdateModal(): React.ReactElement { + const [releaseInfo, setReleaseInfo] = React.useState<{ + version: string; + changelog: string | null; + } | null>(null); + + React.useEffect(() => { + fetchLatestReleaseInfo().then((releaseInfo) => setReleaseInfo(releaseInfo)); + }, []); + + return ( +
+
+

{t("updateModal.description")}

+ + {t("updateModal.currentVersion", { version: MARKETPLACE_VERSION })} + + + {t("updateModal.latestVersion", { version: releaseInfo?.version })} + +
+
+
+

+ {t("updateModal.whatsChanged")} +

+
+ {t("updateModal.seeChangelog")} +
    +
+
+
+
+

{t("updateModal.howToUpgrade")}

+ + {t("updateModal.viewGuide")} + +
+
+ ); +} + +export default UpdateModal; diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index 0cd454ee..b25a33b0 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -86,11 +86,13 @@ export const TopBarContent = (props: { } if (tabBar && topBarContent && Spicetify.Platform.History.location.pathname === "/marketplace") { topBarContent.appendChild(tabBar); + document.querySelector(".main-topBar-container")?.setAttribute("style", "contain: unset;"); } Spicetify.Platform.History.listen(({ pathname }) => { if (pathname != "/marketplace") { // Delete tabBar from the dom document.querySelector(".marketplace-tabBar")?.remove(); + document.querySelector(".main-topBar-container")?.removeAttribute("style"); } }); }; diff --git a/src/constants.ts b/src/constants.ts index acedc827..f11b85be 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -43,7 +43,6 @@ export const SNIPPETS_PAGE_URL = export const BLACKLIST_URL = "https://raw.githubusercontent.com/spicetify/spicetify-marketplace/main/resources/blacklist.json"; -export const LATEST_RELEASE = - "https://api.github.com/repos/spicetify/spicetify-marketplace/releases"; +export const RELEASES_URL = "https://github.com/spicetify/spicetify-marketplace/releases"; -export const RELEASE_CHANGELOG = `https://api.github.com/repos/spicetify/spicetify-marketplace/releases/tags/v${MARKETPLACE_VERSION}`; +export const LATEST_RELEASE_URL = "https://api.github.com/repos/spicetify/spicetify-marketplace/releases/latest"; diff --git a/src/logic/LaunchModals.tsx b/src/logic/LaunchModals.tsx index cf41545e..c262bc98 100644 --- a/src/logic/LaunchModals.tsx +++ b/src/logic/LaunchModals.tsx @@ -7,9 +7,10 @@ import ReloadModal from "../components/Modals/Reload"; import SettingsModal from "../components/Modals/Settings"; import ThemeDevToolsModal from "../components/Modals/ThemeDevTools"; import BackupModal from "../components/Modals/BackupModal"; +import UpdateModal from "../components/Modals/Update"; import { CardProps } from "../components/Card/Card"; -export type ModalType = "ADD_SNIPPET" | "EDIT_SNIPPET" | "VIEW_SNIPPET" | "RELOAD" | "SETTINGS" | "THEME_DEV_TOOLS" | "BACKUP"; +export type ModalType = "ADD_SNIPPET" | "EDIT_SNIPPET" | "VIEW_SNIPPET" | "RELOAD" | "SETTINGS" | "THEME_DEV_TOOLS" | "BACKUP" | "UPDATE"; const getModalSettings = ( modalType: ModalType, @@ -62,6 +63,12 @@ const getModalSettings = ( content: , isLarge: true, }; + case "UPDATE": + return { + title: t("updateModal.title"), + content: , + isLarge: true, + }; default: return { diff --git a/src/logic/Utils.ts b/src/logic/Utils.ts index 30bc74b8..8f9cd3a0 100644 --- a/src/logic/Utils.ts +++ b/src/logic/Utils.ts @@ -1,5 +1,5 @@ import { CardProps } from "../components/Card/Card"; -import { Author, CardItem, ColourScheme, SchemeIni, Snippet, SortBoxOption } from "../types/marketplace-types"; +import { Author, CardItem, ColourScheme, SchemeIni, Snippet, SortBoxOption, ResetCategory } from "../types/marketplace-types"; import Chroma from "chroma-js"; import { LOCALSTORAGE_KEYS } from "../constants"; /** @@ -213,11 +213,12 @@ export const generateSortOptions = (t: (key: string) => string) => { { key: "z-a", value: t("grid.sort.zToA") }, ]; }; + /** * Reset Marketplace localStorage keys * @param categories The categories to reset. If none provided, reset everything. */ -export const resetMarketplace = (...categories: ("extensions" | "snippets" | "theme")[]) => { +export const resetMarketplace = (...categories: ResetCategory[]) => { console.debug("Resetting Marketplace"); const keysToRemove: string[] = []; @@ -235,18 +236,28 @@ export const resetMarketplace = (...categories: ("extensions" | "snippets" | "th // If have categories, reset only those categories.forEach((category) => { - if (category === "extensions") { + switch (category) { + case "extensions": // Remove the extensions themselves keysToRemove.push(...getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedExtensions, [])); // Remove the list of extension keys keysToRemove.push(LOCALSTORAGE_KEYS.installedExtensions); - } else if (category === "snippets") { + break; + + case "snippets": keysToRemove.push(...getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedSnippets, [])); keysToRemove.push(LOCALSTORAGE_KEYS.installedSnippets); - } else if (category === "theme") { + break; + + case "theme": keysToRemove.push(...getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedThemes, [])); keysToRemove.push(LOCALSTORAGE_KEYS.installedThemes); keysToRemove.push(LOCALSTORAGE_KEYS.themeInstalled); + break; + + default: + console.error(`Unknown category: ${category}`); + break; } }); diff --git a/src/resources/locales/en.json b/src/resources/locales/en.json index 6ff413e8..69eeb18a 100644 --- a/src/resources/locales/en.json +++ b/src/resources/locales/en.json @@ -72,6 +72,16 @@ "colorIniEditorPlaceholder": "[your-colour-scheme-name]", "invalidCSS": "Invalid CSS" }, + "updateModal": { + "title": "Update the Marketplace", + "description": "Update Spicetify Marketplace to receive new features and bug fixes.", + "currentVersion": "Current version: {{version}}", + "latestVersion": "Latest version: {{version}}", + "whatsChanged": "What's Changed", + "seeChangelog": "See changelog", + "howToUpgrade": "How to upgrade", + "viewGuide": "View install guide" + }, "grid": { "spicetifyMarketplace": "Spicetify Marketplace", "newUpdate": "New update", diff --git a/src/resources/locales/ru.json b/src/resources/locales/ru.json index 795c244b..ca451a4f 100644 --- a/src/resources/locales/ru.json +++ b/src/resources/locales/ru.json @@ -72,6 +72,16 @@ "colorIniEditorPlaceholder": "[название-вашей-цветовой-схемы]", "invalidCSS": "Неверный CSS" }, + "updateModal": { + "title": "Обновление Маркетплейса", + "description": "Обновите Маркетплейс для получения новых функций и исправлений.", + "currentVersion": "Текущая версия: {{version}}", + "latestVersion": "Последняя версия: {{version}}", + "whatsChanged": "Что нового", + "seeChangelog": "Посмотреть изменения", + "howToUpgrade": "Инструкция по обновлению", + "viewGuide": "Посмотреть инструкцию" + }, "grid": { "spicetifyMarketplace": "Маркетплейс Spicetify", "newUpdate": "Доступно обновление", diff --git a/src/resources/snippets.ts b/src/resources/snippets.ts index fedc9e74..f00187ac 100644 --- a/src/resources/snippets.ts +++ b/src/resources/snippets.ts @@ -329,4 +329,10 @@ export default [ "code": "div.x-downloadButton-DownloadButton { display: none; }", "preview": "resources/assets/snippets/hide-download-button.png", }, + { + "title": "Smaller right sidebar covert art", + "description": "Makes the right sidebar cover art smaller and move the track info to the right", + "code": ":root { --right-sidebar-cover-art-size: 85px; } \n.main-nowPlayingView-coverArt { width: var(--right-sidebar-cover-art-size); } \n.zL6hQR4mukVUUQaa_7K1 { min-height: unset !important; height: var(--right-sidebar-cover-art-size) !important; } \n.main-nowPlayingView-nowPlayingGrid { flex-direction: unset; } \n.main-nowPlayingView-contextItemInfo .main-trackInfo-name { font-size: 1.25rem; } \n.main-nowPlayingView-contextItemInfo .main-trackInfo-artists { font-size: 0.85rem; }", + "preview": "resources/assets/snippets/smaller-right-sidebar-cover.png", + }, ]; diff --git a/src/styles/components/_update-modal.scss b/src/styles/components/_update-modal.scss new file mode 100644 index 00000000..97b7b4ca --- /dev/null +++ b/src/styles/components/_update-modal.scss @@ -0,0 +1,33 @@ +#marketplace-update-container .marketplace-update-header { + margin-bottom: 0.25rem; + margin-top: 0.5rem; +} + +#marketplace-update-description { + & > h4 { + margin-bottom: 0.5rem; + } + + a { + display: block; + } +} + +#marketplace-update-whats-changed { + li { + list-style-type: disc; + + &::marker { + unicode-bidi: isolate; + font-variant-numeric: tabular-nums; + text-transform: none; + text-indent: 0px !important; + text-align: start !important; + text-align-last: start !important; + } + } + + & > details > ul { + padding-left: 1.25rem; + } +} diff --git a/src/styles/styles.scss b/src/styles/styles.scss index 4d992a78..3a1724e7 100644 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -6,12 +6,16 @@ so we don't interfere with other extension like reddit */ -@import url('./components/_grid.scss'); -@import url('./components/_dropdown.scss'); -@import url('./components/_card.scss'); -@import url('./components/_settings.scss'); -@import url('./components/_reload-modal.scss'); -@import url('./components/_add-snippet-modal.scss'); -@import url('./components/_readme-pages.scss'); -@import url('./components/_fixes.scss'); -@import url('./components/_import-export.scss'); +@use './components/prismjs-themes/prism-tomorrow.scss'; +@use './components/_add-snippet-modal.scss'; +@use './components/_backup.scss'; +@use './components/_card.scss'; +@use './components/_code-editors.scss'; +@use './components/_devtools.scss'; +@use './components/_dropdown.scss'; +@use './components/_fixes.scss'; +@use './components/_grid.scss'; +@use './components/_readme-pages.scss'; +@use './components/_reload-modal.scss'; +@use './components/_settings.scss'; +@use './components/_update-modal.scss'; diff --git a/src/types/marketplace-types.d.ts b/src/types/marketplace-types.d.ts index 0473dfad..0e845e76 100644 --- a/src/types/marketplace-types.d.ts +++ b/src/types/marketplace-types.d.ts @@ -1,4 +1,3 @@ - declare global { interface Window { Marketplace: Record; @@ -16,9 +15,19 @@ export type SortBoxOption = { value: string; }; -export type RepoTopic = "spicetify-extensions" | "spicetify-themes" | "spicetify-apps"; +export type RepoTopic = + | "spicetify-extensions" + | "spicetify-themes" + | "spicetify-apps"; + +export type TabType = + | "Extensions" + | "Themes" + | "Snippets" + | "Apps" + | "Installed"; -export type TabType = "Extensions" | "Themes" | "Snippets" | "Apps" | "Installed"; +export type ResetCategory = "extensions" | "snippets" | "theme"; export type CardType = "extension" | "theme" | "snippet" | "app"; @@ -121,15 +130,25 @@ export type VisualConfig = { colorShift: boolean; themeDevTools: boolean; albumArtBasedColors: boolean; - albumArtBasedColorsMode: "monochromeLight" | "monochromeDark" | "quad" | "triad" | "analogic" | "analogicComplement"; - albumArtBasedColorsVibrancy: "DESATURATED" | "LIGHT_VIBRANT" | "PROMINENT" | "VIBRANT"; + albumArtBasedColorsMode: + | "monochromeLight" + | "monochromeDark" + | "quad" + | "triad" + | "analogic" + | "analogicComplement"; + albumArtBasedColorsVibrancy: + | "DESATURATED" + | "LIGHT_VIBRANT" + | "PROMINENT" + | "VIBRANT"; // Legacy from reddit app type: boolean; // I was considering adding watchers as "followers" but it looks like the value is a duplicate // of stargazers, and the subscribers_count isn't returned in the main API call we make // https://github.community/t/bug-watchers-count-is-the-duplicate-of-stargazers-count/140865/4 followers: boolean; -} +}; // example colour scheme // const exampleSchemes = { @@ -154,17 +173,24 @@ export type SchemeIni = { [key: string]: ColourScheme; }; -export type SortMode = "a-z" | "z-a" | "newest" | "oldest" | "stars" | "lastUpdated" | "mostStale"; +export type SortMode = + | "a-z" + | "z-a" + | "newest" + | "oldest" + | "stars" + | "lastUpdated" + | "mostStale"; export type Config = { // Fetch the settings and set defaults. Used in Settings.js - visual: VisualConfig, - tabs: TabItemConfig[], + visual: VisualConfig; + tabs: TabItemConfig[]; activeTab: string; theme: { activeThemeKey?: string; schemes?: SchemeIni; activeScheme?: string | null; - }, + }; sort: SortMode; }; diff --git a/src/types/spicetify.d.ts b/src/types/spicetify.d.ts index 8423281a..9d133f92 100644 --- a/src/types/spicetify.d.ts +++ b/src/types/spicetify.d.ts @@ -1,96 +1,337 @@ -/* eslint-disable */ - declare namespace Spicetify { - type Icon = "album" | "artist" | "block" | "brightness" | "car" | "chart-down" | "chart-up" | "check" | "check-alt-fill" | "chevron-left" | "chevron-right" | "chromecast-disconnected" | "clock" | "collaborative" | "computer" | "copy" | "download" | "downloaded" | "edit" | "enhance" | "exclamation-circle" | "external-link" | "facebook" | "follow" | "fullscreen" | "gamepad" | "grid-view" | "heart" | "heart-active" | "instagram" | "laptop" | "library" | "list-view" | "location" | "locked" | "locked-active" | "lyrics" | "menu" | "minimize" | "minus" | "more" | "new-spotify-connect" | "offline" | "pause" | "phone" | "play" | "playlist" | "playlist-folder" | "plus-alt" | "plus2px" | "podcasts" | "projector" | "queue" | "repeat" | "repeat-once" | "search" | "search-active" | "shuffle" | "skip-back" | "skip-back15" | "skip-forward" | "skip-forward15" | "soundbetter" | "speaker" | "spotify" | "subtitles" | "tablet" | "ticket" | "twitter" | "visualizer" | "voice" | "volume" | "volume-off" | "volume-one-wave" | "volume-two-wave" | "watch" | "x"; - type Variant = "bass" | "forte" | "brio" | "altoBrio" | "alto" | "canon" | "celloCanon" | "cello" | "ballad" | "balladBold" | "viola" | "violaBold" | "mesto" | "mestoBold" | "metronome" | "finale" | "finaleBold" | "minuet" | "minuetBold"; - type SemanticColor = "textBase" | "textSubdued" | "textBrightAccent" | "textNegative" | "textWarning" | "textPositive" | "textAnnouncement" | "essentialBase" | "essentialSubdued" | "essentialBrightAccent" | "essentialNegative" | "essentialWarning" | "essentialPositive" | "essentialAnnouncement" | "decorativeBase" | "decorativeSubdued" | "backgroundBase" | "backgroundHighlight" | "backgroundPress" | "backgroundElevatedBase" | "backgroundElevatedHighlight" | "backgroundElevatedPress" | "backgroundTintedBase" | "backgroundTintedHighlight" | "backgroundTintedPress" | "backgroundUnsafeForSmallTextBase" | "backgroundUnsafeForSmallTextHighlight" | "backgroundUnsafeForSmallTextPress"; - type Metadata = Partial>; - type ContextTrack = { - uri: string; - uid?: string; - metadata?: Metadata; + type Icon = + | "album" + | "artist" + | "block" + | "brightness" + | "car" + | "chart-down" + | "chart-up" + | "check" + | "check-alt-fill" + | "chevron-left" + | "chevron-right" + | "chromecast-disconnected" + | "clock" + | "collaborative" + | "computer" + | "copy" + | "download" + | "downloaded" + | "edit" + | "enhance" + | "exclamation-circle" + | "external-link" + | "facebook" + | "follow" + | "fullscreen" + | "gamepad" + | "grid-view" + | "heart" + | "heart-active" + | "instagram" + | "laptop" + | "library" + | "list-view" + | "location" + | "locked" + | "locked-active" + | "lyrics" + | "menu" + | "minimize" + | "minus" + | "more" + | "new-spotify-connect" + | "offline" + | "pause" + | "phone" + | "play" + | "playlist" + | "playlist-folder" + | "plus-alt" + | "plus2px" + | "podcasts" + | "projector" + | "queue" + | "repeat" + | "repeat-once" + | "search" + | "search-active" + | "shuffle" + | "skip-back" + | "skip-back15" + | "skip-forward" + | "skip-forward15" + | "soundbetter" + | "speaker" + | "spotify" + | "subtitles" + | "tablet" + | "ticket" + | "twitter" + | "visualizer" + | "voice" + | "volume" + | "volume-off" + | "volume-one-wave" + | "volume-two-wave" + | "watch" + | "x"; + type Variant = + | "bass" + | "forte" + | "brio" + | "altoBrio" + | "alto" + | "canon" + | "celloCanon" + | "cello" + | "ballad" + | "balladBold" + | "viola" + | "violaBold" + | "mesto" + | "mestoBold" + | "metronome" + | "finale" + | "finaleBold" + | "minuet" + | "minuetBold"; + type SemanticColor = + | "textBase" + | "textSubdued" + | "textBrightAccent" + | "textNegative" + | "textWarning" + | "textPositive" + | "textAnnouncement" + | "essentialBase" + | "essentialSubdued" + | "essentialBrightAccent" + | "essentialNegative" + | "essentialWarning" + | "essentialPositive" + | "essentialAnnouncement" + | "decorativeBase" + | "decorativeSubdued" + | "backgroundBase" + | "backgroundHighlight" + | "backgroundPress" + | "backgroundElevatedBase" + | "backgroundElevatedHighlight" + | "backgroundElevatedPress" + | "backgroundTintedBase" + | "backgroundTintedHighlight" + | "backgroundTintedPress" + | "backgroundUnsafeForSmallTextBase" + | "backgroundUnsafeForSmallTextHighlight" + | "backgroundUnsafeForSmallTextPress"; + type ColorSet = + | "base" + | "brightAccent" + | "negative" + | "warning" + | "positive" + | "announcement" + | "invertedDark" + | "invertedLight" + | "mutedAccent" + | "overMedia"; + type ColorSetBackgroundColors = { + base: string; + highlight: string; + press: string; + }; + type ColorSetNamespaceColors = { + announcement: string; + base: string; + brightAccent: string; + negative: string; + positive: string; + subdued: string; + warning: string; + }; + type ColorSetBody = { + background: ColorSetBackgroundColors & { + elevated: ColorSetBackgroundColors; + tinted: ColorSetBackgroundColors; + unsafeForSmallText: ColorSetBackgroundColors; }; - type ProvidedTrack = ContextTrack & { - removed?: string[]; - blocked?: string[]; - provider?: string; + decorative: { + base: string; + subdued: string; }; - type ContextOption = { - contextURI?: string; - index?: number; - trackUri?: string; - page?: number; - trackUid?: string; - sortedBy?: string; - filteredBy?: string; - shuffleContext?: boolean; - repeatContext?: boolean; - repeatTrack?: boolean; - offset?: number; - next_page_url?: string; - restrictions?: Record; - referrer?: string; + essential: ColorSetNamespaceColors; + text: ColorSetNamespaceColors; + }; + type Metadata = Partial>; + type ContextTrack = { + uri: string; + uid?: string; + metadata?: Metadata; + }; + type PlayerState = { + timestamp: number; + context: PlayerContext; + index: PlayerIndex; + item: PlayerTrack; + shuffle: boolean; + repeat: number; + speed: number; + positionAsOfTimestamp: number; + duration: number; + hasContext: boolean; + isPaused: boolean; + isBuffering: boolean; + restrictions: Restrictions; + previousItems?: PlayerTrack[]; + nextItems?: PlayerTrack[]; + playbackQuality: PlaybackQuality; + playbackId: string; + sessionId: string; + signals?: any[]; + /** + * @deprecated Use `item` instead. This will be removed in the future. + */ + track: PlayerTrack; + }; + type PlayerContext = { + uri: string; + url: string; + metadata: { + "player.arch": string; }; - type PlayerState = { - timestamp: number; - context_uri: string; - context_url: string; - context_restrictions: Record; - index?: { - page: number; - track: number; - }; - track?: ProvidedTrack; - playback_id?: string; - playback_quality?: string; - playback_speed?: number; - position_as_of_timestamp: number; - duration: number; - is_playing: boolean; - is_paused: boolean; - is_buffering: boolean; - play_origin: { - feature_identifier: string; - feature_version: string; - view_uri?: string; - external_referrer?: string; - referrer_identifier?: string; - device_identifier?: string; - }; - options: { - shuffling_context?: boolean; - repeating_context?: boolean; - repeating_track?: boolean; - }; - restrictions: Record; - suppressions: { - providers: string[]; - }; - debug: { - log: string[]; - }; - prev_tracks: ProvidedTrack[]; - next_tracks: ProvidedTrack[]; - context_metadata: Metadata; - page_metadata: Metadata; - session_id: string; - queue_revision: string; + }; + type PlayerIndex = { + pageURI?: string | null; + pageIndex: number; + itemIndex: number; + }; + type PlayerTrack = { + type: string; + uri: string; + uid: string; + name: string; + mediaType: string; + duration: { + milliseconds: number; }; - namespace Player { - /** - * Register a listener `type` on Spicetify.Player. - * - * On default, `Spicetify.Player` always dispatch: - * - `songchange` type when player changes track. - * - `onplaypause` type when player plays or pauses. - * - `onprogress` type when track progress changes. - * - `appchange` type when user changes page. - */ - function addEventListener(type: string, callback: (event?: Event) => void): void; - function addEventListener(type: "songchange", callback: (event?: Event & { data: PlayerState }) => void): void; - function addEventListener(type: "onplaypause", callback: (event?: Event & { data: PlayerState }) => void): void; - function addEventListener(type: "onprogress", callback: (event?: Event & { data: number }) => void): void; - function addEventListener(type: "appchange", callback: (event?: Event & { data: { + album: Album; + artists?: ArtistsEntity[]; + isLocal: boolean; + isExplicit: boolean; + is19PlusOnly: boolean; + provider: string; + metadata: TrackMetadata; + images?: ImagesEntity[]; + }; + type TrackMetadata = { + artist_uri: string; + entity_uri: string; + iteration: string; + title: string; + "collection.is_banned": string; + "artist_uri:1": string; + "collection.in_collection": string; + image_small_url: string; + "collection.can_ban": string; + is_explicit: string; + album_disc_number: string; + album_disc_count: string; + track_player: string; + album_title: string; + "collection.can_add": string; + image_large_url: string; + "actions.skipping_prev_past_track": string; + page_instance_id: string; + image_xlarge_url: string; + marked_for_download: string; + "actions.skipping_next_past_track": string; + context_uri: string; + "artist_name:1": string; + has_lyrics: string; + interaction_id: string; + image_url: string; + album_uri: string; + album_artist_name: string; + album_track_number: string; + artist_name: string; + duration: string; + album_track_count: string; + popularity: string; + }; + type Album = { + type: string; + uri: string; + name: string; + images?: ImagesEntity[]; + }; + type ImagesEntity = { + url: string; + label: string; + }; + type ArtistsEntity = { + type: string; + uri: string; + name: string; + }; + type Restrictions = { + canPause: boolean; + canResume: boolean; + canSeek: boolean; + canSkipPrevious: boolean; + canSkipNext: boolean; + canToggleRepeatContext: boolean; + canToggleRepeatTrack: boolean; + canToggleShuffle: boolean; + disallowPausingReasons?: string[]; + disallowResumingReasons?: string[]; + disallowSeekingReasons?: string[]; + disallowSkippingPreviousReasons?: string[]; + disallowSkippingNextReasons?: string[]; + disallowTogglingRepeatContextReasons?: string[]; + disallowTogglingRepeatTrackReasons?: string[]; + disallowTogglingShuffleReasons?: string[]; + disallowTransferringPlaybackReasons?: string[]; + }; + type PlaybackQuality = { + bitrateLevel: number; + strategy: number; + targetBitrateLevel: number; + targetBitrateAvailable: boolean; + hifiStatus: number; + }; + namespace Player { + /** + * Register a listener `type` on Spicetify.Player. + * + * On default, `Spicetify.Player` always dispatch: + * - `songchange` type when player changes track. + * - `onplaypause` type when player plays or pauses. + * - `onprogress` type when track progress changes. + * - `appchange` type when user changes page. + */ + function addEventListener( + type: string, + callback: (event?: Event) => void + ): void; + function addEventListener( + type: "songchange", + callback: (event?: Event & { data: PlayerState }) => void + ): void; + function addEventListener( + type: "onplaypause", + callback: (event?: Event & { data: PlayerState }) => void + ): void; + function addEventListener( + type: "onprogress", + callback: (event?: Event & { data: number }) => void + ): void; + function addEventListener( + type: "appchange", + callback: ( + event?: Event & { + data: { /** * App href path */ @@ -98,1852 +339,2442 @@ declare namespace Spicetify { /** * App container */ - container: HTMLElement; - } }) => void): void; - /** - * Skip to previous track. - */ - function back(): void; - /** - * An object contains all information about current track and player. - */ - const data: PlayerState; - /** - * Decrease a small amount of volume. - */ - function decreaseVolume(): void; - /** - * Dispatches an event at `Spicetify.Player`. - * - * On default, `Spicetify.Player` always dispatch - * - `songchange` type when player changes track. - * - `onplaypause` type when player plays or pauses. - * - `onprogress` type when track progress changes. - * - `appchange` type when user changes page. - */ - function dispatchEvent(event: Event): void; - const eventListeners: { - [key: string]: Array<(event?: Event) => void> - }; - /** - * Convert milisecond to `mm:ss` format - * @param milisecond - */ - function formatTime(milisecond: number): string; - /** - * Return song total duration in milisecond. - */ - function getDuration(): number; - /** - * Return mute state - */ - function getMute(): boolean; - /** - * Return elapsed duration in milisecond. - */ - function getProgress(): number; - /** - * Return elapsed duration in percentage (0 to 1). - */ - function getProgressPercent(): number; - /** - * Return current Repeat state (No repeat = 0/Repeat all = 1/Repeat one = 2). - */ - function getRepeat(): number; - /** - * Return current shuffle state. - */ - function getShuffle(): boolean; - /** - * Return track heart state. - */ - function getHeart(): boolean; - /** - * Return current volume level (0 to 1). - */ - function getVolume(): number; - /** - * Increase a small amount of volume. - */ - function increaseVolume(): void; - /** - * Return a boolean whether player is playing. - */ - function isPlaying(): boolean; - /** - * Skip to next track. - */ - function next(): void; - /** - * Pause track. - */ - function pause(): void; - /** - * Resume track. - */ - function play(): void; - /** - * Play a track, playlist, album, etc. immediately - * @param uri Spotify URI - * @param context - * @param options - */ - function playUri(uri: string, context?: any, options?: any): Promise; - /** - * Unregister added event listener `type`. - * @param type - * @param callback - */ - function removeEventListener(type: string, callback: (event?: Event) => void): void; - /** - * Seek track to position. - * @param position can be in percentage (0 to 1) or in milisecond. - */ - function seek(position: number): void; - /** - * Turn mute on/off - * @param state - */ - function setMute(state: boolean): void; - /** - * Change Repeat mode - * @param mode `0` No repeat. `1` Repeat all. `2` Repeat one track. - */ - function setRepeat(mode: number): void; - /** - * Turn shuffle on/off. - * @param state - */ - function setShuffle(state: boolean): void; - /** - * Set volume level - * @param level 0 to 1 - */ - function setVolume(level: number): void; - /** - * Seek to previous `amount` of milisecond - * @param amount in milisecond. Default: 15000. - */ - function skipBack(amount?: number): void; - /** - * Seek to next `amount` of milisecond - * @param amount in milisecond. Default: 15000. - */ - function skipForward(amount?: number): void; - /** - * Toggle Heart (Favourite) track state. - */ - function toggleHeart(): void; - /** - * Toggle Mute/No mute. - */ - function toggleMute(): void; - /** - * Toggle Play/Pause. - */ - function togglePlay(): void; - /** - * Toggle No repeat/Repeat all/Repeat one. - */ - function toggleRepeat(): void; - /** - * Toggle Shuffle/No shuffle. - */ - function toggleShuffle(): void; - } + container: HTMLElement; + }; + } + ) => void + ): void; /** - * Adds a track or array of tracks to prioritized queue. + * Skip to previous track. */ - function addToQueue(uri: ContextTrack[]): Promise; + function back(): void; /** - * @deprecated + * An object contains all information about current track and player. */ - const BridgeAPI: any; + const data: PlayerState; /** - * @deprecated + * Decrease a small amount of volume. */ - const CosmosAPI: any; + function decreaseVolume(): void; /** - * Async wrappers of CosmosAPI + * Dispatches an event at `Spicetify.Player`. + * + * On default, `Spicetify.Player` always dispatch + * - `songchange` type when player changes track. + * - `onplaypause` type when player plays or pauses. + * - `onprogress` type when track progress changes. + * - `appchange` type when user changes page. */ - namespace CosmosAsync { - type Method = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "SUB"; - interface Error { - code: number; - error: string; - message: string; - stack?: string; - } - - type Headers = Record; - type Body = Record; + function dispatchEvent(event: Event): void; + const eventListeners: { + [key: string]: Array<(event?: Event) => void>; + }; + /** + * Convert milisecond to `mm:ss` format + * @param milisecond + */ + function formatTime(milisecond: number): string; + /** + * Return song total duration in milisecond. + */ + function getDuration(): number; + /** + * Return mute state + */ + function getMute(): boolean; + /** + * Return elapsed duration in milisecond. + */ + function getProgress(): number; + /** + * Return elapsed duration in percentage (0 to 1). + */ + function getProgressPercent(): number; + /** + * Return current Repeat state (No repeat = 0/Repeat all = 1/Repeat one = 2). + */ + function getRepeat(): number; + /** + * Return current shuffle state. + */ + function getShuffle(): boolean; + /** + * Return track heart state. + */ + function getHeart(): boolean; + /** + * Return current volume level (0 to 1). + */ + function getVolume(): number; + /** + * Increase a small amount of volume. + */ + function increaseVolume(): void; + /** + * Return a boolean whether player is playing. + */ + function isPlaying(): boolean; + /** + * Skip to next track. + */ + function next(): void; + /** + * Pause track. + */ + function pause(): void; + /** + * Resume track. + */ + function play(): void; + /** + * Play a track, playlist, album, etc. immediately + * @param uri Spotify URI + * @param context + * @param options + */ + function playUri(uri: string, context?: any, options?: any): Promise; + /** + * Unregister added event listener `type`. + * @param type + * @param callback + */ + function removeEventListener( + type: string, + callback: (event?: Event) => void + ): void; + /** + * Seek track to position. + * @param position can be in percentage (0 to 1) or in milisecond. + */ + function seek(position: number): void; + /** + * Turn mute on/off + * @param state + */ + function setMute(state: boolean): void; + /** + * Change Repeat mode + * @param mode `0` No repeat. `1` Repeat all. `2` Repeat one track. + */ + function setRepeat(mode: number): void; + /** + * Turn shuffle on/off. + * @param state + */ + function setShuffle(state: boolean): void; + /** + * Set volume level + * @param level 0 to 1 + */ + function setVolume(level: number): void; + /** + * Seek to previous `amount` of milisecond + * @param amount in milisecond. Default: 15000. + */ + function skipBack(amount?: number): void; + /** + * Seek to next `amount` of milisecond + * @param amount in milisecond. Default: 15000. + */ + function skipForward(amount?: number): void; + /** + * Toggle Heart (Favourite) track state. + */ + function toggleHeart(): void; + /** + * Toggle Mute/No mute. + */ + function toggleMute(): void; + /** + * Toggle Play/Pause. + */ + function togglePlay(): void; + /** + * Toggle No repeat/Repeat all/Repeat one. + */ + function toggleRepeat(): void; + /** + * Toggle Shuffle/No shuffle. + */ + function toggleShuffle(): void; + } + /** + * Adds a track or array of tracks to prioritized queue. + */ + function addToQueue(uri: ContextTrack[]): Promise; + /** + * @deprecated + */ + const BridgeAPI: any; + /** + * @deprecated + */ + const CosmosAPI: any; + /** + * Async wrappers of CosmosAPI + */ + namespace CosmosAsync { + type Method = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "SUB"; + interface Error { + code: number; + error: string; + message: string; + stack?: string; + } - interface Response { - body: any; - headers: Headers; - status: number; - uri?: string; - } + type Headers = Record; + type Body = Record; - function head(url: string, headers?: Headers): Promise; - function get(url: string, body?: Body, headers?: Headers): Promise; - function post(url: string, body?: Body, headers?: Headers): Promise; - function put(url: string, body?: Body, headers?: Headers): Promise; - function del(url: string, body?: Body, headers?: Headers): Promise; - function patch(url: string, body?: Body, headers?: Headers): Promise; - function sub(url: string, callback: ((b: Response["body"]) => void), onError?: ((e: Error) => void), body?: Body, headers?: Headers): Promise; - function postSub(url: string, body: Body | null, callback: ((b: Response["body"]) => void), onError?: ((e: Error) => void)): Promise; - function request(method: Method, url: string, body?: Body, headers?: Headers): Promise; - function resolve(method: Method, url: string, body?: Body, headers?: Headers): Promise; + interface Response { + body: any; + headers: Headers; + status: number; + uri?: string; } + + function head(url: string, headers?: Headers): Promise; + function get( + url: string, + body?: Body, + headers?: Headers + ): Promise; + function post( + url: string, + body?: Body, + headers?: Headers + ): Promise; + function put( + url: string, + body?: Body, + headers?: Headers + ): Promise; + function del( + url: string, + body?: Body, + headers?: Headers + ): Promise; + function patch( + url: string, + body?: Body, + headers?: Headers + ): Promise; + function sub( + url: string, + callback: (b: Response["body"]) => void, + onError?: (e: Error) => void, + body?: Body, + headers?: Headers + ): Promise; + function postSub( + url: string, + body: Body | null, + callback: (b: Response["body"]) => void, + onError?: (e: Error) => void + ): Promise; + function request( + method: Method, + url: string, + body?: Body, + headers?: Headers + ): Promise; + function resolve( + method: Method, + url: string, + body?: Body, + headers?: Headers + ): Promise; + } + /** + * Fetch interesting colors from URI. + * @param uri Any type of URI that has artwork (playlist, track, album, artist, show, ...) + */ + function colorExtractor(uri: string): Promise<{ + DESATURATED: string; + LIGHT_VIBRANT: string; + PROMINENT: string; + VIBRANT: string; + VIBRANT_NON_ALARMING: string; + }>; + /** + * @deprecated + */ + function getAblumArtColors(): any; + /** + * Fetch track analyzed audio data. + * Beware, not all tracks have audio data. + * @param uri is optional. Leave it blank to get current track + * or specify another track uri. + */ + function getAudioData(uri?: string): Promise; + /** + * Set of APIs method to register, deregister hotkeys/shortcuts + */ + namespace Keyboard { + type ValidKey = + | "BACKSPACE" + | "TAB" + | "ENTER" + | "SHIFT" + | "CTRL" + | "ALT" + | "CAPS" + | "ESCAPE" + | "SPACE" + | "PAGE_UP" + | "PAGE_DOWN" + | "END" + | "HOME" + | "ARROW_LEFT" + | "ARROW_UP" + | "ARROW_RIGHT" + | "ARROW_DOWN" + | "INSERT" + | "DELETE" + | "A" + | "B" + | "C" + | "D" + | "E" + | "F" + | "G" + | "H" + | "I" + | "J" + | "K" + | "L" + | "M" + | "N" + | "O" + | "P" + | "Q" + | "R" + | "S" + | "T" + | "U" + | "V" + | "W" + | "X" + | "Y" + | "Z" + | "WINDOW_LEFT" + | "WINDOW_RIGHT" + | "SELECT" + | "NUMPAD_0" + | "NUMPAD_1" + | "NUMPAD_2" + | "NUMPAD_3" + | "NUMPAD_4" + | "NUMPAD_5" + | "NUMPAD_6" + | "NUMPAD_7" + | "NUMPAD_8" + | "NUMPAD_9" + | "MULTIPLY" + | "ADD" + | "SUBTRACT" + | "DECIMAL_POINT" + | "DIVIDE" + | "F1" + | "F2" + | "F3" + | "F4" + | "F5" + | "F6" + | "F7" + | "F8" + | "F9" + | "F10" + | "F11" + | "F12" + | ";" + | "=" + | " | " + | "-" + | "." + | "/" + | "`" + | "[" + | "\\" + | "]" + | '"' + | "~" + | "!" + | "@" + | "#" + | "$" + | "%" + | "^" + | "&" + | "*" + | "(" + | ")" + | "_" + | "+" + | ":" + | "<" + | ">" + | "?" + | "|"; + type KeysDefine = + | string + | { + key: string; + ctrl?: boolean; + shift?: boolean; + alt?: boolean; + meta?: boolean; + }; + const KEYS: Record; + function registerShortcut( + keys: KeysDefine, + callback: (event: KeyboardEvent) => void + ): void; + function registerIsolatedShortcut( + keys: KeysDefine, + callback: (event: KeyboardEvent) => void + ): void; + function registerImportantShortcut( + keys: KeysDefine, + callback: (event: KeyboardEvent) => void + ): void; + function _deregisterShortcut(keys: KeysDefine): void; + function deregisterImportantShortcut(keys: KeysDefine): void; + function changeShortcut(keys: KeysDefine, newKeys: KeysDefine): void; + } + + /** + * @deprecated + */ + const LiveAPI: any; + + namespace LocalStorage { /** - * Fetch interesting colors from URI. - * @param uri Any type of URI that has artwork (playlist, track, album, artist, show, ...) + * Empties the list associated with the object of all key/value pairs, if there are any. */ - function colorExtractor(uri: string): Promise<{ - DESATURATED: string; - LIGHT_VIBRANT: string; - PROMINENT: string; - VIBRANT: string; - VIBRANT_NON_ALARMING: string; - }>; + function clear(): void; /** - * @deprecated + * Get key value */ - function getAblumArtColors(): any; + function get(key: string): string | null; /** - * Fetch track analyzed audio data. - * Beware, not all tracks have audio data. - * @param uri is optional. Leave it blank to get current track - * or specify another track uri. + * Delete key */ - function getAudioData(uri?: string): Promise; + function remove(key: string): void; /** - * Set of APIs method to register, deregister hotkeys/shortcuts + * Set new value for key */ - namespace Keyboard { - type ValidKey = "BACKSPACE" | "TAB" | "ENTER" | "SHIFT" | "CTRL" | "ALT" | "CAPS" | "ESCAPE" | "SPACE" | "PAGE_UP" | "PAGE_DOWN" | "END" | "HOME" | "ARROW_LEFT" | "ARROW_UP" | "ARROW_RIGHT" | "ARROW_DOWN" | "INSERT" | "DELETE" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "WINDOW_LEFT" | "WINDOW_RIGHT" | "SELECT" | "NUMPAD_0" | "NUMPAD_1" | "NUMPAD_2" | "NUMPAD_3" | "NUMPAD_4" | "NUMPAD_5" | "NUMPAD_6" | "NUMPAD_7" | "NUMPAD_8" | "NUMPAD_9" | "MULTIPLY" | "ADD" | "SUBTRACT" | "DECIMAL_POINT" | "DIVIDE" | "F1" | "F2" | "F3" | "F4" | "F5" | "F6" | "F7" | "F8" | "F9" | "F10" | "F11" | "F12" | ";" | "=" | " | " | "-" | "." | "/" | "`" | "[" | "\\" | "]" | "\"" | "~" | "!" | "@" | "#" | "$" | "%" | "^" | "&" | "*" | "(" | ")" | "_" | "+" | ":" | "<" | ">" | "?" | "|"; - type KeysDefine = string | { - key: string; - ctrl?: boolean; - shift?: boolean; - alt?: boolean; - meta?: boolean; - }; - const KEYS: Record; - function registerShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; - function registerIsolatedShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; - function registerImportantShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void; - function _deregisterShortcut(keys: KeysDefine): void; - function deregisterImportantShortcut(keys: KeysDefine): void; - function changeShortcut(keys: KeysDefine, newKeys: KeysDefine): void; + function set(key: string, value: string): void; + } + /** + * To create and prepend custom menu item in profile menu. + */ + namespace Menu { + /** + * Create a single toggle. + */ + class Item { + constructor( + name: string, + isEnabled: boolean, + onClick: (self: Item) => void, + icon?: Icon | string + ); + name: string; + isEnabled: boolean; + /** + * Change item name + */ + setName(name: string): void; + /** + * Change item enabled state. + * Visually, item would has a tick next to it if its state is enabled. + */ + setState(isEnabled: boolean): void; + /** + * Change icon + */ + setIcon(icon: Icon | string): void; + /** + * Item is only available in Profile menu when method "register" is called. + */ + register(): void; + /** + * Stop item to be prepended into Profile menu. + */ + deregister(): void; } /** - * @deprecated - */ - const LiveAPI: any; - - namespace LocalStorage { - /** - * Empties the list associated with the object of all key/value pairs, if there are any. - */ - function clear(): void; - /** - * Get key value - */ - function get(key: string): string | null; - /** - * Delete key - */ - function remove(key: string): void; - /** - * Set new value for key - */ - function set(key: string, value: string): void; + * Create a sub menu to contain Item toggles. + * `Item`s in `subItems` array shouldn't be registered. + */ + class SubMenu { + constructor(name: string, subItems: Item[]); + name: string; + /** + * Change SubMenu name + */ + setName(name: string): void; + /** + * Add an item to sub items list + */ + addItem(item: Item); + /** + * Remove an item from sub items list + */ + removeItem(item: Item); + /** + * SubMenu is only available in Profile menu when method "register" is called. + */ + register(): void; + /** + * Stop SubMenu to be prepended into Profile menu. + */ + deregister(): void; } + } + + /** + * Keyboard shortcut library + * + * Documentation: https://craig.is/killing/mice v1.6.5 + * + * Spicetify.Keyboard is wrapper of this library to be compatible with legacy Spotify, + * so new extension should use this library instead. + */ + function Mousetrap(element?: any): void; + + /** + * Contains vast array of internal APIs. + * Please explore in Devtool Console. + */ + const Platform: any; + /** + * Queue object contains list of queuing tracks, + * history of played tracks and current track metadata. + */ + const Queue: { + nextTracks: any[]; + prevTracks: any[]; + queueRevision: string; + track: any; + }; + /** + * Remove a track or array of tracks from current queue. + */ + function removeFromQueue(uri: ContextTrack[]): Promise; + /** + * Display a bubble of notification. Useful for a visual feedback. + * @param message Message to display. Can use inline HTML for styling. + * @param isError If true, bubble will be red. Defaults to false. + * @param msTimeout Time in milliseconds to display the bubble. Defaults to Spotify's value. + */ + function showNotification( + message: React.ReactNode, + isError?: boolean, + msTimeout?: number + ): void; + /** + * Set of APIs method to parse and validate URIs. + */ + class URI { + constructor(type: string, props: any); + public type: string; + public hasBase62Id: boolean; + + public id?: string; + public disc?: any; + public args?: any; + public category?: string; + public username?: string; + public track?: string; + public artist?: string; + public album?: string; + public duration?: number; + public query?: string; + public country?: string; + public global?: boolean; + public context?: string | typeof URI | null; + public anchor?: string; + public play?: any; + public toplist?: any; + /** - * To create and prepend custom menu item in profile menu. + * + * @return The URI representation of this uri. */ - namespace Menu { - /** - * Create a single toggle. - */ - class Item { - constructor(name: string, isEnabled: boolean, onClick: (self: Item) => void, icon?: Icon | string); - name: string; - isEnabled: boolean; - /** - * Change item name - */ - setName(name: string): void; - /** - * Change item enabled state. - * Visually, item would has a tick next to it if its state is enabled. - */ - setState(isEnabled: boolean): void; - /** - * Change icon - */ - setIcon(icon: Icon | string): void; - /** - * Item is only available in Profile menu when method "register" is called. - */ - register(): void; - /** - * Stop item to be prepended into Profile menu. - */ - deregister(): void; - } + toURI(): string; - /** - * Create a sub menu to contain Item toggles. - * `Item`s in `subItems` array shouldn't be registered. - */ - class SubMenu { - constructor(name: string, subItems: Item[]); - name: string; - /** - * Change SubMenu name - */ - setName(name: string): void; - /** - * Add an item to sub items list - */ - addItem(item: Item); - /** - * Remove an item from sub items list - */ - removeItem(item: Item); - /** - * SubMenu is only available in Profile menu when method "register" is called. - */ - register(): void; - /** - * Stop SubMenu to be prepended into Profile menu. - */ - deregister(): void; - } - } + /** + * + * @return The URI representation of this uri. + */ + toString(): string; + + /** + * Get the URL path of this uri. + * + * @param opt_leadingSlash True if a leading slash should be prepended. + * @return The path of this uri. + */ + toURLPath(opt_leadingSlash: boolean): string; /** - * Keyboard shortcut library * - * Documentation: https://craig.is/killing/mice v1.6.5 + * @param origin The origin to use for the URL. + * @return The URL string for the uri. + */ + toURL(origin?: string): string; + + /** + * Clones a given SpotifyURI instance. * - * Spicetify.Keyboard is wrapper of this library to be compatible with legacy Spotify, - * so new extension should use this library instead. + * @return An instance of URI. */ - function Mousetrap(element?: any): void; + clone(): URI | null; /** - * Contains vast array of internal APIs. - * Please explore in Devtool Console. + * Gets the path of the URI object by removing all hash and query parameters. + * + * @return The path of the URI object. */ - const Platform: any; + getPath(): string; + /** - * Queue object contains list of queuing tracks, - * history of played tracks and current track metadata. + * The various URI Types. + * + * Note that some of the types in this enum are not real URI types, but are + * actually URI particles. They are marked so. + * */ - const Queue: { - nextTracks: any[]; - prevTracks: any[]; - queueRevision: string; - track: any; + static Type: { + AD: string; + ALBUM: string; + GENRE: string; + QUEUE: string; + APPLICATION: string; + ARTIST: string; + ARTIST_TOPLIST: string; + ARTIST_CONCERTS: string; + AUDIO_FILE: string; + COLLECTION: string; + COLLECTION_ALBUM: string; + COLLECTION_ARTIST: string; + COLLECTION_MISSING_ALBUM: string; + COLLECTION_TRACK_LIST: string; + CONCERT: string; + CONTEXT_GROUP: string; + DAILY_MIX: string; + EMPTY: string; + EPISODE: string; + /** URI particle; not an actual URI. */ + FACEBOOK: string; + FOLDER: string; + FOLLOWERS: string; + FOLLOWING: string; + IMAGE: string; + INBOX: string; + INTERRUPTION: string; + LIBRARY: string; + LIVE: string; + ROOM: string; + EXPRESSION: string; + LOCAL: string; + LOCAL_TRACK: string; + LOCAL_ALBUM: string; + LOCAL_ARTIST: string; + MERCH: string; + MOSAIC: string; + PLAYLIST: string; + PLAYLIST_V2: string; + PRERELEASE: string; + PROFILE: string; + PUBLISHED_ROOTLIST: string; + RADIO: string; + ROOTLIST: string; + SEARCH: string; + SHOW: string; + SOCIAL_SESSION: string; + SPECIAL: string; + STARRED: string; + STATION: string; + TEMP_PLAYLIST: string; + TOPLIST: string; + TRACK: string; + TRACKSET: string; + USER_TOPLIST: string; + USER_TOP_TRACKS: string; + UNKNOWN: string; + MEDIA: string; + QUESTION: string; + POLL: string; }; + /** - * Remove a track or array of tracks from current queue. - */ - function removeFromQueue(uri: ContextTrack[]): Promise; - /** - * Display a bubble of notification. Useful for a visual feedback. - * @param message Message to display. Can use inline HTML for styling. - * @param isError If true, bubble will be red. Defaults to false. - * @param msTimeout Time in milliseconds to display the bubble. Defaults to Spotify's value. - */ - function showNotification(message: React.ReactNode, isError?: boolean, msTimeout?: number): void; - /** - * Set of APIs method to parse and validate URIs. - */ - class URI { - constructor(type: string, props: any); - public type: string; - public hasBase62Id: boolean; - - public id?: string; - public disc?: any; - public args?: any; - public category?: string; - public username?: string; - public track?: string; - public artist?: string; - public album?: string; - public duration?: number; - public query?: string; - public country?: string; - public global?: boolean; - public context?: string | typeof URI | null; - public anchor?: string; - public play?: any; - public toplist?: any; - - /** - * - * @return The URI representation of this uri. - */ - toURI(): string; - - /** - * - * @return The URI representation of this uri. - */ - toString(): string; - - /** - * Get the URL path of this uri. - * - * @param opt_leadingSlash True if a leading slash should be prepended. - * @return The path of this uri. - */ - toURLPath(opt_leadingSlash: boolean): string; - - /** - * - * @param origin The origin to use for the URL. - * @return The URL string for the uri. - */ - toURL(origin?: string): string; - - /** - * Clones a given SpotifyURI instance. - * - * @return An instance of URI. - */ - clone(): URI | null; - - /** - * Gets the path of the URI object by removing all hash and query parameters. - * - * @return The path of the URI object. - */ - getPath(): string; - - /** - * The various URI Types. - * - * Note that some of the types in this enum are not real URI types, but are - * actually URI particles. They are marked so. - * - */ - static Type: { - AD: string; - ALBUM: string; - GENRE: string; - QUEUE: string; - APPLICATION: string; - ARTIST: string; - ARTIST_TOPLIST: string; - ARTIST_CONCERTS: string; - AUDIO_FILE: string; - COLLECTION: string; - COLLECTION_ALBUM: string; - COLLECTION_ARTIST: string; - COLLECTION_MISSING_ALBUM: string; - COLLECTION_TRACK_LIST: string; - CONCERT: string; - CONTEXT_GROUP: string; - DAILY_MIX: string; - EMPTY: string; - EPISODE: string; - /** URI particle; not an actual URI. */ - FACEBOOK: string; - FOLDER: string; - FOLLOWERS: string; - FOLLOWING: string; - IMAGE: string; - INBOX: string; - INTERRUPTION: string; - LIBRARY: string; - LIVE: string; - ROOM: string; - EXPRESSION: string; - LOCAL: string; - LOCAL_TRACK: string; - LOCAL_ALBUM: string; - LOCAL_ARTIST: string; - MERCH: string; - MOSAIC: string; - PLAYLIST: string; - PLAYLIST_V2: string; - PRERELEASE: string; - PROFILE: string; - PUBLISHED_ROOTLIST: string; - RADIO: string; - ROOTLIST: string; - SEARCH: string; - SHOW: string; - SOCIAL_SESSION: string; - SPECIAL: string; - STARRED: string; - STATION: string; - TEMP_PLAYLIST: string; - TOPLIST: string; - TRACK: string; - TRACKSET: string; - USER_TOPLIST: string; - USER_TOP_TRACKS: string; - UNKNOWN: string; - MEDIA: string; - QUESTION: string; - POLL: string; - }; + * Creates a new URI object from a parsed string argument. + * + * @param str The string that will be parsed into a URI object. + * @throws TypeError If the string argument is not a valid URI, a TypeError will + * be thrown. + * @return The parsed URI object. + */ + static fromString(str: string): URI; - /** - * Creates a new URI object from a parsed string argument. - * - * @param str The string that will be parsed into a URI object. - * @throws TypeError If the string argument is not a valid URI, a TypeError will - * be thrown. - * @return The parsed URI object. - */ - static fromString(str: string): URI; - - /** - * Parses a given object into a URI instance. - * - * Unlike URI.fromString, this function could receive any kind of value. If - * the value is already a URI instance, it is simply returned. - * Otherwise the value will be stringified before parsing. - * - * This function also does not throw an error like URI.fromString, but - * instead simply returns null if it can't parse the value. - * - * @param value The value to parse. - * @return The corresponding URI instance, or null if the - * passed value is not a valid value. - */ - static from(value: any): URI | null; - - /** - * Checks whether two URI:s refer to the same thing even though they might - * not necessarily be equal. - * - * These two Playlist URIs, for example, refer to the same playlist: - * - * spotify:user:napstersean:playlist:3vxotOnOGDlZXyzJPLFnm2 - * spotify:playlist:3vxotOnOGDlZXyzJPLFnm2 - * - * @param baseUri The first URI to compare. - * @param refUri The second URI to compare. - * @return Whether they shared idenitity - */ - static isSameIdentity(baseUri: URI | string, refUri: URI | string): boolean; - - /** - * Returns the hex representation of a Base62 encoded id. - * - * @param id The base62 encoded id. - * @return The hex representation of the base62 id. - */ - static idToHex(id: string): string; - - /** - * Returns the base62 representation of a hex encoded id. - * - * @param hex The hex encoded id. - * @return The base62 representation of the id. - */ - static hexToId(hex: string): string; - - /** - * Creates a new 'album' type URI. - * - * @param id The id of the album. - * @param disc The disc number of the album. - * @return The album URI. - */ - static albumURI(id: string, disc: number): URI; - - /** - * Creates a new 'application' type URI. - * - * @param id The id of the application. - * @param args An array containing the arguments to the app. - * @return The application URI. - */ - static applicationURI(id: string, args: string[]): URI; - - /** - * Creates a new 'artist' type URI. - * - * @param id The id of the artist. - * @return The artist URI. - */ - static artistURI(id: string): URI; - - /** - * Creates a new 'collection' type URI. - * - * @param username The non-canonical username of the rootlist owner. - * @param category The category of the collection. - * @return The collection URI. - */ - static collectionURI(username: string, category: string): URI; - - /** - * Creates a new 'collection-album' type URI. - * - * @param username The non-canonical username of the rootlist owner. - * @param id The id of the album. - * @return The collection album URI. - */ - static collectionAlbumURI(username: string, id: string): URI; - - /** - * Creates a new 'collection-artist' type URI. - * - * @param username The non-canonical username of the rootlist owner. - * @param id The id of the artist. - * @return The collection artist URI. - */ - static collectionAlbumURI(username: string, id: string): URI; - - /** - * Creates a new 'concert' type URI. - * - * @param id The id of the concert. - * @return The concert URI. - */ - static concertURI(id: string): URI; - - /** - * Creates a new 'episode' type URI. - * - * @param id The id of the episode. - * @return The episode URI. - */ - static episodeURI(id: string): URI; - - /** - * Creates a new 'folder' type URI. - * - * @param id The id of the folder. - * @return The folder URI. - */ - static folderURI(id: string): URI; - - /** - * Creates a new 'local-album' type URI. - * - * @param artist The artist of the album. - * @param album The name of the album. - * @return The local album URI. - */ - static localAlbumURI(artist: string, album: string): URI; - - /** - * Creates a new 'local-artist' type URI. - * - * @param artist The name of the artist. - * @return The local artist URI. - */ - static localArtistURI(artist: string): URI; - - /** - * Creates a new 'playlist-v2' type URI. - * - * @param id The id of the playlist. - * @return The playlist URI. - */ - static playlistV2URI(id: string): URI; - - /** - * Creates a new 'prerelease' type URI. - * - * @param id The id of the prerelease. - * @return The prerelease URI. - */ - static prereleaseURI(id: string): URI; - - /** - * Creates a new 'profile' type URI. - * - * @param username The non-canonical username of the rootlist owner. - * @param args A list of arguments. - * @return The profile URI. - */ - static profileURI(username: string, args: string[]): URI; - - /** - * Creates a new 'search' type URI. - * - * @param query The unencoded search query. - * @return The search URI - */ - static searchURI(query: string): URI; - - /** - * Creates a new 'show' type URI. - * - * @param id The id of the show. - * @return The show URI. - */ - static showURI(id: string): URI; - - /** - * Creates a new 'station' type URI. - * - * @param args An array of arguments for the station. - * @return The station URI. - */ - static stationURI(args: string[]): URI; - - /** - * Creates a new 'track' type URI. - * - * @param id The id of the track. - * @param anchor The point in the track formatted as mm:ss - * @param context An optional context URI - * @param play Toggles autoplay - * @return The track URI. - */ - static trackURI(id: string, anchor: string, context?: string, play: boolean): URI; - - /** - * Creates a new 'user-toplist' type URI. - * - * @param username The non-canonical username of the toplist owner. - * @param toplist The toplist type. - * @return The user-toplist URI. - */ - static userToplistURI(username: string, toplist: string): URI; - - static isAd(uri: URI | string): boolean; - static isAlbum(uri: URI | string): boolean; - static isGenre(uri: URI | string): boolean; - static isQueue(uri: URI | string): boolean; - static isApplication(uri: URI | string): boolean; - static isArtist(uri: URI | string): boolean; - static isArtistToplist(uri: URI | string): boolean; - static isArtistConcerts(uri: URI | string): boolean; - static isAudioFile(uri: URI | string): boolean; - static isCollection(uri: URI | string): boolean; - static isCollectionAlbum(uri: URI | string): boolean; - static isCollectionArtist(uri: URI | string): boolean; - static isCollectionMissingAlbum(uri: URI | string): boolean; - static isCollectionTrackList(uri: URI | string): boolean; - static isConcert(uri: URI | string): boolean; - static isContextGroup(uri: URI | string): boolean; - static isDailyMix(uri: URI | string): boolean; - static isEmpty(uri: URI | string): boolean; - static isEpisode(uri: URI | string): boolean; - static isFacebook(uri: URI | string): boolean; - static isFolder(uri: URI | string): boolean; - static isFollowers(uri: URI | string): boolean; - static isFollowing(uri: URI | string): boolean; - static isImage(uri: URI | string): boolean; - static isInbox(uri: URI | string): boolean; - static isInterruption(uri: URI | string): boolean; - static isLibrary(uri: URI | string): boolean; - static isLive(uri: URI | string): boolean; - static isRoom(uri: URI | string): boolean; - static isExpression(uri: URI | string): boolean; - static isLocal(uri: URI | string): boolean; - static isLocalTrack(uri: URI | string): boolean; - static isLocalAlbum(uri: URI | string): boolean; - static isLocalArtist(uri: URI | string): boolean; - static isMerch(uri: URI | string): boolean; - static isMosaic(uri: URI | string): boolean; - static isPlaylist(uri: URI | string): boolean; - static isPlaylistV2(uri: URI | string): boolean; - static isPrerelease(uri: URI | string): boolean; - static isProfile(uri: URI | string): boolean; - static isPublishedRootlist(uri: URI | string): boolean; - static isRadio(uri: URI | string): boolean; - static isRootlist(uri: URI | string): boolean; - static isSearch(uri: URI | string): boolean; - static isShow(uri: URI | string): boolean; - static isSocialSession(uri: URI | string): boolean; - static isSpecial(uri: URI | string): boolean; - static isStarred(uri: URI | string): boolean; - static isStation(uri: URI | string): boolean; - static isTempPlaylist(uri: URI | string): boolean; - static isToplist(uri: URI | string): boolean; - static isTrack(uri: URI | string): boolean; - static isTrackset(uri: URI | string): boolean; - static isUserToplist(uri: URI | string): boolean; - static isUserTopTracks(uri: URI | string): boolean; - static isUnknown(uri: URI | string): boolean; - static isMedia(uri: URI | string): boolean; - static isQuestion(uri: URI | string): boolean; - static isPoll(uri: URI | string): boolean; - static isPlaylistV1OrV2(uri: URI | string): boolean; - } + /** + * Parses a given object into a URI instance. + * + * Unlike URI.fromString, this function could receive any kind of value. If + * the value is already a URI instance, it is simply returned. + * Otherwise the value will be stringified before parsing. + * + * This function also does not throw an error like URI.fromString, but + * instead simply returns null if it can't parse the value. + * + * @param value The value to parse. + * @return The corresponding URI instance, or null if the + * passed value is not a valid value. + */ + static from(value: any): URI | null; /** - * Create custom menu item and prepend to right click context menu + * Checks whether two URI:s refer to the same thing even though they might + * not necessarily be equal. + * + * These two Playlist URIs, for example, refer to the same playlist: + * + * spotify:user:napstersean:playlist:3vxotOnOGDlZXyzJPLFnm2 + * spotify:playlist:3vxotOnOGDlZXyzJPLFnm2 + * + * @param baseUri The first URI to compare. + * @param refUri The second URI to compare. + * @return Whether they shared idenitity */ - namespace ContextMenu { - type OnClickCallback = (uris: string[], uids?: string[], contextUri?: string) => void; - type ShouldAddCallback = (uris: string[], uids?: string[], contextUri?: string) => boolean; + static isSameIdentity(baseUri: URI | string, refUri: URI | string): boolean; - // Single context menu item - class Item { - /** - * List of valid icons to use. - */ - static readonly iconList: Icon[]; - constructor(name: string, onClick: OnClickCallback, shouldAdd?: ShouldAddCallback, icon?: Icon, disabled?: boolean); - name: string; - icon: Icon | string; - disabled: boolean; - /** - * A function returning boolean determines whether item should be prepended. - */ - shouldAdd: ShouldAddCallback; - /** - * A function to call when item is clicked - */ - onClick: OnClickCallback; - /** - * Item is only available in Context Menu when method "register" is called. - */ - register: () => void; - /** - * Stop Item to be prepended into Context Menu. - */ - deregister: () => void; - } + /** + * Returns the hex representation of a Base62 encoded id. + * + * @param id The base62 encoded id. + * @return The hex representation of the base62 id. + */ + static idToHex(id: string): string; - /** - * Create a sub menu to contain `Item`s. - * `Item`s in `subItems` array shouldn't be registered. - */ - class SubMenu { - constructor(name: string, subItems: Iterable, shouldAdd?: ShouldAddCallback, disabled?: boolean); - name: string; - disabled: boolean; - /** - * A function returning boolean determines whether item should be prepended. - */ - shouldAdd: ShouldAddCallback; - addItem: (item: Item) => void; - removeItem: (item: Item) => void; - /** - * SubMenu is only available in Context Menu when method "register" is called. - */ - register: () => void; - /** - * Stop SubMenu to be prepended into Context Menu. - */ - deregister: () => void; - } - } + /** + * Returns the base62 representation of a hex encoded id. + * + * @param hex The hex encoded id. + * @return The base62 representation of the id. + */ + static hexToId(hex: string): string; + + /** + * Creates a new 'album' type URI. + * + * @param id The id of the album. + * @param disc The disc number of the album. + * @return The album URI. + */ + static albumURI(id: string, disc: number): URI; /** - * Popup Modal + * Creates a new 'application' type URI. + * + * @param id The id of the application. + * @param args An array containing the arguments to the app. + * @return The application URI. */ - namespace PopupModal { - interface Content { - title: string; - /** - * You can specify a string for simple text display - * or a HTML element for interactive config/setting menu - */ - content: string | Element; - /** - * Bigger window - */ - isLarge?: boolean; - } + static applicationURI(id: string, args: string[]): URI; + + /** + * Creates a new 'artist' type URI. + * + * @param id The id of the artist. + * @return The artist URI. + */ + static artistURI(id: string): URI; + + /** + * Creates a new 'collection' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param category The category of the collection. + * @return The collection URI. + */ + static collectionURI(username: string, category: string): URI; + + /** + * Creates a new 'collection-album' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param id The id of the album. + * @return The collection album URI. + */ + static collectionAlbumURI(username: string, id: string): URI; + + /** + * Creates a new 'collection-artist' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param id The id of the artist. + * @return The collection artist URI. + */ + static collectionAlbumURI(username: string, id: string): URI; + + /** + * Creates a new 'concert' type URI. + * + * @param id The id of the concert. + * @return The concert URI. + */ + static concertURI(id: string): URI; + + /** + * Creates a new 'episode' type URI. + * + * @param id The id of the episode. + * @return The episode URI. + */ + static episodeURI(id: string): URI; + + /** + * Creates a new 'folder' type URI. + * + * @param id The id of the folder. + * @return The folder URI. + */ + static folderURI(id: string): URI; + + /** + * Creates a new 'local-album' type URI. + * + * @param artist The artist of the album. + * @param album The name of the album. + * @return The local album URI. + */ + static localAlbumURI(artist: string, album: string): URI; + + /** + * Creates a new 'local-artist' type URI. + * + * @param artist The name of the artist. + * @return The local artist URI. + */ + static localArtistURI(artist: string): URI; + + /** + * Creates a new 'playlist-v2' type URI. + * + * @param id The id of the playlist. + * @return The playlist URI. + */ + static playlistV2URI(id: string): URI; + + /** + * Creates a new 'prerelease' type URI. + * + * @param id The id of the prerelease. + * @return The prerelease URI. + */ + static prereleaseURI(id: string): URI; + + /** + * Creates a new 'profile' type URI. + * + * @param username The non-canonical username of the rootlist owner. + * @param args A list of arguments. + * @return The profile URI. + */ + static profileURI(username: string, args: string[]): URI; + + /** + * Creates a new 'search' type URI. + * + * @param query The unencoded search query. + * @return The search URI + */ + static searchURI(query: string): URI; + + /** + * Creates a new 'show' type URI. + * + * @param id The id of the show. + * @return The show URI. + */ + static showURI(id: string): URI; + + /** + * Creates a new 'station' type URI. + * + * @param args An array of arguments for the station. + * @return The station URI. + */ + static stationURI(args: string[]): URI; + + /** + * Creates a new 'track' type URI. + * + * @param id The id of the track. + * @param anchor The point in the track formatted as mm:ss + * @param context An optional context URI + * @param play Toggles autoplay + * @return The track URI. + */ + static trackURI( + id: string, + anchor: string, + context?: string, + play?: boolean + ): URI; - function display(e: Content): void; - function hide(): void; + /** + * Creates a new 'user-toplist' type URI. + * + * @param username The non-canonical username of the toplist owner. + * @param toplist The toplist type. + * @return The user-toplist URI. + */ + static userToplistURI(username: string, toplist: string): URI; + + static isAd(uri: URI | string): boolean; + static isAlbum(uri: URI | string): boolean; + static isGenre(uri: URI | string): boolean; + static isQueue(uri: URI | string): boolean; + static isApplication(uri: URI | string): boolean; + static isArtist(uri: URI | string): boolean; + static isArtistToplist(uri: URI | string): boolean; + static isArtistConcerts(uri: URI | string): boolean; + static isAudioFile(uri: URI | string): boolean; + static isCollection(uri: URI | string): boolean; + static isCollectionAlbum(uri: URI | string): boolean; + static isCollectionArtist(uri: URI | string): boolean; + static isCollectionMissingAlbum(uri: URI | string): boolean; + static isCollectionTrackList(uri: URI | string): boolean; + static isConcert(uri: URI | string): boolean; + static isContextGroup(uri: URI | string): boolean; + static isDailyMix(uri: URI | string): boolean; + static isEmpty(uri: URI | string): boolean; + static isEpisode(uri: URI | string): boolean; + static isFacebook(uri: URI | string): boolean; + static isFolder(uri: URI | string): boolean; + static isFollowers(uri: URI | string): boolean; + static isFollowing(uri: URI | string): boolean; + static isImage(uri: URI | string): boolean; + static isInbox(uri: URI | string): boolean; + static isInterruption(uri: URI | string): boolean; + static isLibrary(uri: URI | string): boolean; + static isLive(uri: URI | string): boolean; + static isRoom(uri: URI | string): boolean; + static isExpression(uri: URI | string): boolean; + static isLocal(uri: URI | string): boolean; + static isLocalTrack(uri: URI | string): boolean; + static isLocalAlbum(uri: URI | string): boolean; + static isLocalArtist(uri: URI | string): boolean; + static isMerch(uri: URI | string): boolean; + static isMosaic(uri: URI | string): boolean; + static isPlaylist(uri: URI | string): boolean; + static isPlaylistV2(uri: URI | string): boolean; + static isPrerelease(uri: URI | string): boolean; + static isProfile(uri: URI | string): boolean; + static isPublishedRootlist(uri: URI | string): boolean; + static isRadio(uri: URI | string): boolean; + static isRootlist(uri: URI | string): boolean; + static isSearch(uri: URI | string): boolean; + static isShow(uri: URI | string): boolean; + static isSocialSession(uri: URI | string): boolean; + static isSpecial(uri: URI | string): boolean; + static isStarred(uri: URI | string): boolean; + static isStation(uri: URI | string): boolean; + static isTempPlaylist(uri: URI | string): boolean; + static isToplist(uri: URI | string): boolean; + static isTrack(uri: URI | string): boolean; + static isTrackset(uri: URI | string): boolean; + static isUserToplist(uri: URI | string): boolean; + static isUserTopTracks(uri: URI | string): boolean; + static isUnknown(uri: URI | string): boolean; + static isMedia(uri: URI | string): boolean; + static isQuestion(uri: URI | string): boolean; + static isPoll(uri: URI | string): boolean; + static isPlaylistV1OrV2(uri: URI | string): boolean; + } + + /** + * Create custom menu item and prepend to right click context menu + */ + namespace ContextMenu { + type OnClickCallback = ( + uris: string[], + uids?: string[], + contextUri?: string + ) => void; + type ShouldAddCallback = ( + uris: string[], + uids?: string[], + contextUri?: string + ) => boolean; + + // Single context menu item + class Item { + /** + * List of valid icons to use. + */ + static readonly iconList: Icon[]; + constructor( + name: string, + onClick: OnClickCallback, + shouldAdd?: ShouldAddCallback, + icon?: Icon, + disabled?: boolean + ); + name: string; + icon: Icon | string; + disabled: boolean; + /** + * A function returning boolean determines whether item should be prepended. + */ + shouldAdd: ShouldAddCallback; + /** + * A function to call when item is clicked + */ + onClick: OnClickCallback; + /** + * Item is only available in Context Menu when method "register" is called. + */ + register: () => void; + /** + * Stop Item to be prepended into Context Menu. + */ + deregister: () => void; } - /** React instance to create components */ - const React: any; - /** React DOM instance to render and mount components */ - const ReactDOM: any; - /** React DOM Server instance to render components to string */ - const ReactDOMServer: any; + /** + * Create a sub menu to contain `Item`s. + * `Item`s in `subItems` array shouldn't be registered. + */ + class SubMenu { + constructor( + name: string, + subItems: Iterable, + shouldAdd?: ShouldAddCallback, + disabled?: boolean + ); + name: string; + disabled: boolean; + /** + * A function returning boolean determines whether item should be prepended. + */ + shouldAdd: ShouldAddCallback; + addItem: (item: Item) => void; + removeItem: (item: Item) => void; + /** + * SubMenu is only available in Context Menu when method "register" is called. + */ + register: () => void; + /** + * Stop SubMenu to be prepended into Context Menu. + */ + deregister: () => void; + } + } + + /** + * Popup Modal + */ + namespace PopupModal { + interface Content { + title: string; + /** + * You can specify a string for simple text display + * or a HTML element for interactive config/setting menu + */ + content: string | Element; + /** + * Bigger window + */ + isLarge?: boolean; + } - /** Stock React components exposed from Spotify library */ - namespace ReactComponent { - type ContextMenuProps = { - /** - * Decide whether to use the global singleton context menu (rendered in ) - * or a new inline context menu (rendered in a sibling - * element to `children`) - */ - renderInline?: boolean; - /** - * Determins what will trigger the context menu. For example, a click, or a right-click - */ - trigger?: 'click' | 'right-click'; - /** - * Determins is the context menu should open or toggle when triggered - */ - action?: 'toggle' | 'open'; - /** - * The preferred placement of the context menu when it opens. - * Relative to trigger element. - */ - placement?: 'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end'; - /** - * The x and y offset distances at which the context menu should open. - * Relative to trigger element and `position`. - */ - offset?: [number, number]; - /** - * Will stop the client from scrolling while the context menu is open - */ - preventScrollingWhileOpen?: boolean; - /** - * The menu UI to render inside of the context menu. - */ - menu: typeof Spicetify.ReactComponent.Menu | - typeof Spicetify.ReactComponent.AlbumMenu | - typeof Spicetify.ReactComponent.PodcastShowMenu | - typeof Spicetify.ReactComponent.ArtistMenu | - typeof Spicetify.ReactComponent.PlaylistMenu; - /** - * A child of the context menu. Should be `