diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..46c9a9b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+dist/* linguist-vendored
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ce2d6ee
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,144 @@
+
+# Created by https://www.toptal.com/developers/gitignore/api/node
+# Edit at https://www.toptal.com/developers/gitignore?templates=node
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+### Node Patch ###
+# Serverless Webpack directories
+.webpack/
+
+# Optional stylelint cache
+
+# SvelteKit build / generate output
+.svelte-kit
+
+# End of https://www.toptal.com/developers/gitignore/api/node
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e4e9416
--- /dev/null
+++ b/README.md
@@ -0,0 +1,62 @@
+# QuickAddToPlaylist
+
+[Spicetify](https://spicetify.app/) extension to add a shortcut for adding current track to a pre-selected playlist.
+
+## Install
+
+Available from the [Spicetify Marketplace](https://github.com/woosy/spicetify-quick-add-to-playlist) or via direct install:
+
+Copy `quick-add-to-playlist.js` into your [Spicetify](https://github.com/khanhas/spicetify-cli) extensions directory:
+| **Platform** | **Path** |
+|------------|-----------------------------------------------------------------------------------|
+| **Linux** | `~/.config/spicetify/Extensions` or `$XDG_CONFIG_HOME/.config/spicetify/Extensions/` |
+| **MacOS** | `~/spicetify_data/Extensions` or `$SPICETIFY_CONFIG/Extensions` |
+| **Windows** | `%appdata%\spicetify\Extensions\` |
+
+After putting the extension file into the correct folder, run the following command to install the extension:
+```
+spicetify config extensions quick-add-to-playlist.js
+spicetify apply
+```
+Note: using the `config` command to add the extension will always append the file name to the existing extensions list. It does not replace the whole key's value.
+
+Or you can manually edit your `config-xpui.ini` file. Add your desired extension filenames in the extensions key, separated them by the | character.
+Example:
+
+```ini
+[AdditionalOptions]
+...
+extensions = quick-add-to-playlist.js
+```
+
+Then run:
+
+```
+spicetify apply
+```
+
+## Usage Details
+
+The extension adds a new option when right-clicking one of your playlist, to select a playlist
+
+![](https://prnt.sc/eIVMioQFd4aG)
+
+Once you have selected a playlist, you can use the new topbar button to quickly add/remove the current track to the selected playlist
+
+![](https://prnt.sc/jR194OhmzHtY)
+
+## Usage Notes
+
+- If you find any issues, please report them on the [issues page.](https://github.com/woosy/spicetify-quick-add-to-playlist/issues/new/choose)
+
+
+## Upcoming Features
+
+- Settings, to customize the "selected playlist" background color
+
+
+## Credits
+
+Made with Spicetify Creator
+
+- https://github.com/FlafyDev/spicetify-creator
diff --git a/dist/quick-add-to-playlist.js b/dist/quick-add-to-playlist.js
new file mode 100644
index 0000000..7379440
--- /dev/null
+++ b/dist/quick-add-to-playlist.js
@@ -0,0 +1,5 @@
+var quickDaddDtoDplaylist=(()=>{var l="add_to_playlist.playlist",s='',o='',c='';async function n(t){r(t,"?"),await async function(){const i=Spicetify.Player.data.track;var t=d();if(!t)return!1;const e=await Spicetify.CosmosAsync.get("https://api.spotify.com/v1/playlists/"+t.id);return e.tracks.items.some(t=>t.track.uri===i.uri)}()?r(t,"-"):r(t,"+")}function r(t,i){var e=d();switch(i){case"+":t.icon=s,t.label="Add to "+e.name;break;case"-":t.icon=o,t.label="Remove from "+e.name;break;case"?":t.icon=c,t.label="Loading..."}}function p(t,i){var t=document.querySelector(`a[href="/playlist/${t}"`),t=(null!=(t=null==t?void 0:t.parentElement)&&t.classList.remove("quick-add-to-playlist--selected-playlist"),document.querySelector(`a[href="/playlist/${i}"`));null!=(i=null==t?void 0:t.parentElement)&&i.classList.add("quick-add-to-playlist--selected-playlist")}function d(){return JSON.parse(Spicetify.LocalStorage.get(l))}var t=async function(){for(;null==Spicetify||!Spicetify.showNotification;)await new Promise(t=>setTimeout(t,500));{const i=document.body,e=document.createElement("style");i.classList.contains("quick-add-to-playlist--selected-playlist")||(e.innerHTML=`
+ .quick-add-to-playlist--selected-playlist {
+ background-color: #323959 !important;
+ }
+ `,i.appendChild(e));var t=d();t&&p(null,t.id),setTimeout(()=>{var t=document.querySelector(".main-rootlist-wrapper");if(t){const i=new MutationObserver(t=>{for(const e of t){var i;"attributes"!==e.type||"style"!==e.attributeName||(i=d())&&p(null,i.id)}});i.observe(t,{attributes:!0,childList:!0,subtree:!0})}},2e3)}const a=new Spicetify.Topbar.Button("Loading...",c,async t=>{var i=Spicetify.Player.data.track;const e=d();e&&(t.icon===s?Spicetify.CosmosAsync.post(`https://api.spotify.com/v1/playlists/${e.id}/tracks`,{uris:[null==i?void 0:i.uri]}).then(()=>{Spicetify.showNotification("Added to "+e.name),r(a,"-")}).catch(()=>{Spicetify.showNotification("An error has occured!")}):t.icon===o&&Spicetify.CosmosAsync.del(`https://api.spotify.com/v1/playlists/${e.id}/tracks`,{tracks:[{uri:null==i?void 0:i.uri}]}).then(()=>{Spicetify.showNotification("Removed from "+e.name),r(a,"+")}).catch(()=>{Spicetify.showNotification("An error has occured!")}))});await n(a),Spicetify.Player.addEventListener("appchange",async()=>{await n(a)}),Spicetify.Player.addEventListener("songchange",async()=>{await n(a)}),new Spicetify.ContextMenu.Item("Select playlist",async([t],[]=[],i)=>{var e=d(),t=await Spicetify.CosmosAsync.get("https://api.spotify.com/v1/playlists/"+t.split(":")[2]);Spicetify.LocalStorage.set(l,JSON.stringify({name:t.name,id:t.id})),p(e.id||null,t.id),Spicetify.showNotification(`Selected playlist "${t.name}"`)},([t])=>{switch(t.split(":")[1]){case Spicetify.URI.Type.PLAYLIST:case Spicetify.URI.Type.PLAYLIST_V2:return!0;default:return!1}},"playlist-folder").register()};(async()=>{await t()})()})();
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..da7fa6a
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,9 @@
+[
+ {
+ "name": "QuickAddToPlaylist",
+ "description": "Shortcut for creating playlists",
+ "preview": "screenshot.png",
+ "main": "dist/quick-add-to-playlist.js",
+ "readme": "README.md"
+ }
+]
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..083762b
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1438 @@
+{
+ "name": "quick-add-to-playlist",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@adobe/css-tools": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.0.1.tgz",
+ "integrity": "sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g==",
+ "dev": true
+ },
+ "@esbuild/linux-loong64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz",
+ "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==",
+ "dev": true,
+ "optional": true
+ },
+ "@jridgewell/gen-mapping": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
+ "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
+ "@jridgewell/resolve-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "dev": true
+ },
+ "@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true
+ },
+ "@jridgewell/source-map": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
+ "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/gen-mapping": "^0.3.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
+ "@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+ "dev": true
+ },
+ "@jridgewell/trace-mapping": {
+ "version": "0.3.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz",
+ "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "@types/clean-css": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.5.tgz",
+ "integrity": "sha512-NEzjkGGpbs9S9fgC4abuBvTpVwE3i+Acu9BBod3PUyjDVZcNsGx61b8r2PphR61QGPnn0JHVs5ey6/I4eTrkxw==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "source-map": "^0.6.0"
+ }
+ },
+ "@types/glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
+ "dev": true,
+ "requires": {
+ "@types/minimatch": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/html-minifier-terser": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-7.0.0.tgz",
+ "integrity": "sha512-hw3bhStrg5e3FQT8qZKCJTrzt/UbEaunU1xRWJ+aNOTmeBMvE3S4Ml2HiiNnZgL8izu0LFVkHUoPFXL1s5QNpQ==",
+ "dev": true
+ },
+ "@types/minify": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@types/minify/-/minify-8.0.0.tgz",
+ "integrity": "sha512-gjkm4vR7KC4raQ/Q9GmWEtDSOvoUa5tV6e/1zRFMmRFe5er6OSMFlkpIXzQNdBS4Qk6H1Y7OCR8J4ur3s0UapA==",
+ "dev": true,
+ "requires": {
+ "@types/clean-css": "*",
+ "@types/html-minifier-terser": "*",
+ "terser": "^5.3.2"
+ }
+ },
+ "@types/minimatch": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
+ "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "17.0.45",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
+ "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
+ "dev": true
+ },
+ "@types/prop-types": {
+ "version": "15.7.5",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
+ "dev": true
+ },
+ "@types/react": {
+ "version": "18.0.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.17.tgz",
+ "integrity": "sha512-38ETy4tL+rn4uQQi7mB81G7V1g0u2ryquNmsVIOKUAEIDK+3CUjZ6rSRpdvS99dNBnkLFL83qfmtLacGOTIhwQ==",
+ "dev": true,
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@types/react-dom": {
+ "version": "18.0.6",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz",
+ "integrity": "sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/scheduler": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
+ "dev": true
+ },
+ "@types/uglify-js": {
+ "version": "3.16.0",
+ "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.16.0.tgz",
+ "integrity": "sha512-0yeUr92L3r0GLRnBOvtYK1v2SjqMIqQDHMl7GLb+l2L8+6LSFWEEWEIgVsPdMn5ImLM8qzWT8xFPtQYpp8co0g==",
+ "dev": true,
+ "requires": {
+ "source-map": "^0.6.1"
+ }
+ },
+ "acorn": {
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
+ "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dev": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true
+ },
+ "autoprefixer": {
+ "version": "10.4.8",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.8.tgz",
+ "integrity": "sha512-75Jr6Q/XpTqEf6D2ltS5uMewJIx5irCU1oBYJrWjFenq/m12WRRrz6g15L1EIoYvPLXTbEry7rDOwrcYNj77xw==",
+ "dev": true,
+ "requires": {
+ "browserslist": "^4.21.3",
+ "caniuse-lite": "^1.0.30001373",
+ "fraction.js": "^4.2.0",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.0",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "browserslist": {
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz",
+ "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==",
+ "dev": true,
+ "requires": {
+ "caniuse-lite": "^1.0.30001370",
+ "electron-to-chromium": "^1.4.202",
+ "node-releases": "^2.0.6",
+ "update-browserslist-db": "^1.0.5"
+ }
+ },
+ "buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
+ },
+ "caniuse-lite": {
+ "version": "1.0.30001378",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001378.tgz",
+ "integrity": "sha512-JVQnfoO7FK7WvU4ZkBRbPjaot4+YqxogSDosHv0Hv5mWpUESmN+UubMU6L/hGz8QlQ2aY5U0vR6MOs6j/CXpNA==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ }
+ },
+ "clean-css": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz",
+ "integrity": "sha512-lCr8OHhiWCTw4v8POJovCoh4T7I9U11yVsPjMWWnnMmp9ZowCxyad1Pathle/9HjaDp+fdQKjO9fQydE6RHTZg==",
+ "dev": true,
+ "requires": {
+ "source-map": "~0.6.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "copy-anything": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
+ "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
+ "dev": true,
+ "requires": {
+ "is-what": "^3.14.1"
+ }
+ },
+ "cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true
+ },
+ "csstype": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
+ "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==",
+ "dev": true
+ },
+ "cwd": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz",
+ "integrity": "sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==",
+ "dev": true,
+ "requires": {
+ "find-pkg": "^0.1.2",
+ "fs-exists-sync": "^0.1.0"
+ }
+ },
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "electron-to-chromium": {
+ "version": "1.4.222",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.222.tgz",
+ "integrity": "sha512-gEM2awN5HZknWdLbngk4uQCVfhucFAfFzuchP3wM3NN6eow1eDU0dFy2kts43FB20ZfhVFF0jmFSTb1h5OhyIg==",
+ "dev": true
+ },
+ "errno": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
+ "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "prr": "~1.0.1"
+ }
+ },
+ "esbuild": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz",
+ "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
+ "dev": true,
+ "requires": {
+ "@esbuild/linux-loong64": "0.14.54",
+ "esbuild-android-64": "0.14.54",
+ "esbuild-android-arm64": "0.14.54",
+ "esbuild-darwin-64": "0.14.54",
+ "esbuild-darwin-arm64": "0.14.54",
+ "esbuild-freebsd-64": "0.14.54",
+ "esbuild-freebsd-arm64": "0.14.54",
+ "esbuild-linux-32": "0.14.54",
+ "esbuild-linux-64": "0.14.54",
+ "esbuild-linux-arm": "0.14.54",
+ "esbuild-linux-arm64": "0.14.54",
+ "esbuild-linux-mips64le": "0.14.54",
+ "esbuild-linux-ppc64le": "0.14.54",
+ "esbuild-linux-riscv64": "0.14.54",
+ "esbuild-linux-s390x": "0.14.54",
+ "esbuild-netbsd-64": "0.14.54",
+ "esbuild-openbsd-64": "0.14.54",
+ "esbuild-sunos-64": "0.14.54",
+ "esbuild-windows-32": "0.14.54",
+ "esbuild-windows-64": "0.14.54",
+ "esbuild-windows-arm64": "0.14.54"
+ }
+ },
+ "esbuild-android-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz",
+ "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-android-arm64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz",
+ "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-darwin-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz",
+ "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-darwin-arm64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz",
+ "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-freebsd-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz",
+ "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-freebsd-arm64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz",
+ "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-32": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz",
+ "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
+ "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-arm": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz",
+ "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-arm64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz",
+ "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-mips64le": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz",
+ "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-ppc64le": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz",
+ "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-riscv64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz",
+ "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-linux-s390x": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz",
+ "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-netbsd-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz",
+ "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-openbsd-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz",
+ "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-plugin-postcss2": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/esbuild-plugin-postcss2/-/esbuild-plugin-postcss2-0.1.1.tgz",
+ "integrity": "sha512-BMHnOTfZo+ghrzYnBtcXlHuMpOEwfTFarhGG4o6mivzPZGUXzeMn/hdFhtRpmCKzUvXsaxnZ3N72xA8CwdtZ3w==",
+ "dev": true,
+ "requires": {
+ "autoprefixer": "^10.2.5",
+ "fs-extra": "^9.1.0",
+ "less": "^4.x",
+ "postcss": "8.x",
+ "postcss-modules": "^4.0.0",
+ "resolve-file": "^0.3.0",
+ "sass": "^1.x",
+ "stylus": "^0.x",
+ "tmp": "^0.2.1"
+ }
+ },
+ "esbuild-sunos-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz",
+ "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-32": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz",
+ "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz",
+ "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild-windows-arm64": {
+ "version": "0.14.54",
+ "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz",
+ "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==",
+ "dev": true,
+ "optional": true
+ },
+ "escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true
+ },
+ "expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
+ "dev": true,
+ "requires": {
+ "homedir-polyfill": "^1.0.1"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "find-file-up": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz",
+ "integrity": "sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==",
+ "dev": true,
+ "requires": {
+ "fs-exists-sync": "^0.1.0",
+ "resolve-dir": "^0.1.0"
+ }
+ },
+ "find-pkg": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz",
+ "integrity": "sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==",
+ "dev": true,
+ "requires": {
+ "find-file-up": "^0.1.2"
+ }
+ },
+ "fraction.js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
+ "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
+ "dev": true
+ },
+ "fs-exists-sync": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
+ "integrity": "sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==",
+ "dev": true
+ },
+ "fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "requires": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "generic-names": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz",
+ "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^3.2.0"
+ }
+ },
+ "glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "global-modules": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz",
+ "integrity": "sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==",
+ "dev": true,
+ "requires": {
+ "global-prefix": "^0.1.4",
+ "is-windows": "^0.2.0"
+ }
+ },
+ "global-prefix": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz",
+ "integrity": "sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==",
+ "dev": true,
+ "requires": {
+ "homedir-polyfill": "^1.0.0",
+ "ini": "^1.3.4",
+ "is-windows": "^0.2.0",
+ "which": "^1.2.12"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.10",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "homedir-polyfill": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+ "dev": true,
+ "requires": {
+ "parse-passwd": "^1.0.0"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ }
+ },
+ "icss-replace-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz",
+ "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==",
+ "dev": true
+ },
+ "icss-utils": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+ "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+ "dev": true
+ },
+ "image-size": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+ "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
+ "dev": true,
+ "optional": true
+ },
+ "immutable": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
+ "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+ "dev": true
+ },
+ "is-core-module": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz",
+ "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
+ "dev": true
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "is-what": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
+ "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
+ "dev": true
+ },
+ "is-windows": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz",
+ "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.6",
+ "universalify": "^2.0.0"
+ }
+ },
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ },
+ "lazy-cache": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz",
+ "integrity": "sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==",
+ "dev": true,
+ "requires": {
+ "set-getter": "^0.1.0"
+ }
+ },
+ "less": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz",
+ "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==",
+ "dev": true,
+ "requires": {
+ "copy-anything": "^2.0.1",
+ "errno": "^0.1.1",
+ "graceful-fs": "^4.1.2",
+ "image-size": "~0.5.0",
+ "make-dir": "^2.1.0",
+ "mime": "^1.4.1",
+ "needle": "^3.1.0",
+ "parse-node-version": "^1.0.1",
+ "source-map": "~0.6.0",
+ "tslib": "^2.3.0"
+ }
+ },
+ "loader-utils": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz",
+ "integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==",
+ "dev": true
+ },
+ "lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "dev": true
+ },
+ "make-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+ "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "pify": "^4.0.1",
+ "semver": "^5.6.0"
+ }
+ },
+ "mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "optional": true
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "optional": true
+ },
+ "nanoid": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
+ "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
+ "dev": true
+ },
+ "needle": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/needle/-/needle-3.1.0.tgz",
+ "integrity": "sha512-gCE9weDhjVGCRqS8dwDR/D3GTAeyXLXuqp7I8EzH6DllZGXSUyxuqqLh+YX9rMAWaaTFyVAg6rHGL25dqvczKw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "debug": "^3.2.6",
+ "iconv-lite": "^0.6.3",
+ "sax": "^1.2.4"
+ }
+ },
+ "node-releases": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
+ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
+ "dev": true
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ },
+ "normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
+ "dev": true
+ },
+ "parse-node-version": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
+ "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
+ "dev": true
+ },
+ "parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true
+ },
+ "pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "dev": true,
+ "optional": true
+ },
+ "postcss": {
+ "version": "8.4.16",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
+ "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
+ "dev": true,
+ "requires": {
+ "nanoid": "^3.3.4",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "postcss-modules": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.3.1.tgz",
+ "integrity": "sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==",
+ "dev": true,
+ "requires": {
+ "generic-names": "^4.0.0",
+ "icss-replace-symbols": "^1.1.0",
+ "lodash.camelcase": "^4.3.0",
+ "postcss-modules-extract-imports": "^3.0.0",
+ "postcss-modules-local-by-default": "^4.0.0",
+ "postcss-modules-scope": "^3.0.0",
+ "postcss-modules-values": "^4.0.0",
+ "string-hash": "^1.1.1"
+ }
+ },
+ "postcss-modules-extract-imports": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
+ "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
+ "dev": true
+ },
+ "postcss-modules-local-by-default": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz",
+ "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==",
+ "dev": true,
+ "requires": {
+ "icss-utils": "^5.0.0",
+ "postcss-selector-parser": "^6.0.2",
+ "postcss-value-parser": "^4.1.0"
+ }
+ },
+ "postcss-modules-scope": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz",
+ "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==",
+ "dev": true,
+ "requires": {
+ "postcss-selector-parser": "^6.0.4"
+ }
+ },
+ "postcss-modules-values": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+ "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+ "dev": true,
+ "requires": {
+ "icss-utils": "^5.0.0"
+ }
+ },
+ "postcss-selector-parser": {
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+ "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "dev": true,
+ "requires": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true
+ },
+ "prr": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+ "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
+ "dev": true,
+ "optional": true
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "resolve": {
+ "version": "1.22.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+ "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "resolve-dir": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz",
+ "integrity": "sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==",
+ "dev": true,
+ "requires": {
+ "expand-tilde": "^1.2.2",
+ "global-modules": "^0.2.3"
+ },
+ "dependencies": {
+ "expand-tilde": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz",
+ "integrity": "sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==",
+ "dev": true,
+ "requires": {
+ "os-homedir": "^1.0.1"
+ }
+ }
+ }
+ },
+ "resolve-file": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/resolve-file/-/resolve-file-0.3.0.tgz",
+ "integrity": "sha512-9RXicAgDvLD272hZ3HwJv9MJUGxCBRRwwSBRdOGWgcO03MtC9UTGC6XG1VbS4T5MvDrb+tVZx2RhZ90uk3uczg==",
+ "dev": true,
+ "requires": {
+ "cwd": "^0.10.0",
+ "expand-tilde": "^2.0.2",
+ "extend-shallow": "^2.0.1",
+ "fs-exists-sync": "^0.1.0",
+ "homedir-polyfill": "^1.0.1",
+ "lazy-cache": "^2.0.2",
+ "resolve": "^1.2.0"
+ }
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "optional": true
+ },
+ "sass": {
+ "version": "1.54.4",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.4.tgz",
+ "integrity": "sha512-3tmF16yvnBwtlPrNBHw/H907j8MlOX8aTBnlNX1yrKx24RKcJGPyLhFUwkoKBKesR3unP93/2z14Ll8NicwQUA==",
+ "dev": true,
+ "requires": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ }
+ },
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "optional": true
+ },
+ "set-getter": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.1.tgz",
+ "integrity": "sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw==",
+ "dev": true,
+ "requires": {
+ "to-object-path": "^0.3.0"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "dev": true
+ },
+ "source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "spicetify-creator": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/spicetify-creator/-/spicetify-creator-1.0.11.tgz",
+ "integrity": "sha512-4COez/XqS4m82y3llIYRpVWKQygQLwG0saf/s/JiwR0OEge1MGHkThQL+k9wywhKT4vOh+hPmY7O/vF9xDduNw==",
+ "dev": true,
+ "requires": {
+ "@types/glob": "^7.2.0",
+ "@types/minify": "^8.0.0",
+ "@types/node": "^17.0.13",
+ "@types/uglify-js": "^3.13.1",
+ "chalk": "^4.1.2",
+ "clean-css": "^5.2.4",
+ "esbuild": "^0.14.13",
+ "esbuild-plugin-postcss2": "0.1.1",
+ "glob": "^7.2.0",
+ "minimist": "^1.2.5",
+ "uglify-js": "^3.15.1"
+ }
+ },
+ "string-hash": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz",
+ "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==",
+ "dev": true
+ },
+ "stylus": {
+ "version": "0.59.0",
+ "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz",
+ "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==",
+ "dev": true,
+ "requires": {
+ "@adobe/css-tools": "^4.0.1",
+ "debug": "^4.3.2",
+ "glob": "^7.1.6",
+ "sax": "~1.2.4",
+ "source-map": "^0.7.3"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+ "dev": true
+ }
+ }
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true
+ },
+ "terser": {
+ "version": "5.14.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
+ "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/source-map": "^0.3.2",
+ "acorn": "^8.5.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ }
+ },
+ "tmp": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+ "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+ "dev": true,
+ "requires": {
+ "rimraf": "^3.0.0"
+ }
+ },
+ "to-object-path": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
+ "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "tslib": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
+ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
+ "dev": true
+ },
+ "uglify-js": {
+ "version": "3.17.0",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.0.tgz",
+ "integrity": "sha512-aTeNPVmgIMPpm1cxXr2Q/nEbvkmV8yq66F3om7X3P/cvOXQ0TMQ64Wk63iyT1gPlmdmGzjGpyLh1f3y8MZWXGg==",
+ "dev": true
+ },
+ "universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+ "dev": true
+ },
+ "update-browserslist-db": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz",
+ "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==",
+ "dev": true,
+ "requires": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..7102414
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "quick-add-to-playlist",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "build": "spicetify-creator",
+ "build-local": "spicetify-creator --out=dist --minify",
+ "watch": "spicetify-creator --watch"
+ },
+ "license": "MIT",
+ "devDependencies": {
+ "@types/react": "^18.0.17",
+ "@types/react-dom": "^18.0.6",
+ "spicetify-creator": "^1.0.11"
+ }
+}
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..a2604cb
Binary files /dev/null and b/screenshot.png differ
diff --git a/src/app.tsx b/src/app.tsx
new file mode 100644
index 0000000..c878871
--- /dev/null
+++ b/src/app.tsx
@@ -0,0 +1,229 @@
+const LOCALSTORAGE_PLAYLIST_KEY = 'add_to_playlist.playlist'
+
+const ICON_PLUS = ``
+const ICON_MINUS = ``
+const ICON_LOADING = ``
+
+async function main() {
+ // ==============================================================================================================
+ // ==============================================================================================================
+ // ==============================================================================================================
+
+ // ------------------------------------------------------------
+ // Wait for Spotify + Spicetify to load
+
+ while (!Spicetify?.showNotification) {
+ await new Promise(resolve => setTimeout(resolve, 500))
+ }
+
+ // Inject CSS
+ initInjectCss()
+
+ // ------------------------------------------------------------
+ // Create extension Button
+
+ const topBarButton: Spicetify.Topbar.Button = new Spicetify.Topbar.Button(
+ 'Loading...',
+ ICON_LOADING,
+ async (self) => {
+ const track: any = Spicetify.Player.data.track
+ const playlist = getPlaylistFromLocalstorage()
+ if (!playlist) return
+
+ // Add to playlist
+ if (self.icon === ICON_PLUS) {
+ Spicetify.CosmosAsync.post(`https://api.spotify.com/v1/playlists/${playlist.id}/tracks`, { uris: [track?.uri] })
+ .then(() => {
+ Spicetify.showNotification(`Added to ${playlist.name}`)
+ updateTopBarButton(topBarButton, '-')
+ })
+ .catch(() => { Spicetify.showNotification(`An error has occured!`) })
+
+ // Remove from playlist
+ } else if (self.icon === ICON_MINUS) {
+ Spicetify.CosmosAsync.del(`https://api.spotify.com/v1/playlists/${playlist.id}/tracks`, { tracks: [{ uri: track?.uri }] })
+ .then(() => {
+ Spicetify.showNotification(`Removed from ${playlist.name}`)
+ updateTopBarButton(topBarButton, '+')
+ })
+ .catch(() => { Spicetify.showNotification(`An error has occured!`) })
+ }
+ }
+ )
+
+ await updateTopBar(topBarButton)
+
+ // ------------------------------------------------------------
+ // Event listeners: update icon (if already in playlist or not)
+ // ------------------------------------------------------------
+
+ // On app start
+ Spicetify.Player.addEventListener('appchange', async () => {
+ await updateTopBar(topBarButton)
+ })
+
+ // On song change
+ Spicetify.Player.addEventListener("songchange", async () => {
+ await updateTopBar(topBarButton)
+ })
+
+
+ // ==============================================================================================================
+ // ==============================================================================================================
+ // ==============================================================================================================
+
+ // ------------------------------------------------------------
+ // Create option in ContextMenu
+
+ new Spicetify.ContextMenu.Item(
+ 'Select playlist',
+ // Anonymous function called when selecting a playlist
+ async ([uri], [uid] = [], context = undefined) => {
+ const oldPlaylist = getPlaylistFromLocalstorage()
+ const newPlaylist = await Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/playlists/${uri.split(":")[2]}`)
+
+ Spicetify.LocalStorage.set(
+ LOCALSTORAGE_PLAYLIST_KEY,
+ JSON.stringify({
+ name: newPlaylist.name,
+ id: newPlaylist.id
+ })
+ )
+
+ updateStyle(oldPlaylist.id || null, newPlaylist.id)
+ Spicetify.showNotification(`Selected playlist "${newPlaylist.name}"`)
+ },
+ // Enable "Select playlist" only when right-clicking playlists
+ ([uri]) => {
+ const type = uri.split(":")[1]
+ switch (type) {
+ case Spicetify.URI.Type.PLAYLIST:
+ case Spicetify.URI.Type.PLAYLIST_V2:
+ return true
+ default:
+ return false
+ }
+ },
+ 'playlist-folder'
+ ).register()
+}
+
+// ==============================================================================================================
+// ==============================================================================================================
+// ==============================================================================================================
+
+// Check if current track is in playlist
+async function isCurrentTrackInSelectedPlaylist (): Promise {
+ const track: any = Spicetify.Player.data.track
+ const playlist = getPlaylistFromLocalstorage()
+ if (!playlist) return false
+ const updatedPlaylist = await Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/playlists/${playlist.id}`)
+
+ return updatedPlaylist.tracks.items.some((item: any) => item.track.uri === track.uri)
+}
+
+// ==============================================================================================================
+// ==============================================================================================================
+// ==============================================================================================================
+
+// Update Topbar icon/label
+async function updateTopBar (topBarButton: Spicetify.Topbar.Button): Promise {
+ updateTopBarButton(topBarButton, '?')
+ await isCurrentTrackInSelectedPlaylist()
+ ? updateTopBarButton(topBarButton, '-')
+ : updateTopBarButton(topBarButton, '+')
+}
+
+// Update Topbar.Button icon & label
+function updateTopBarButton (topBarButton: Spicetify.Topbar.Button, icon: '+' | '-' | '?'): void {
+ const playlist = getPlaylistFromLocalstorage()
+
+ switch (icon) {
+ case '+':
+ topBarButton.icon = ICON_PLUS
+ topBarButton.label = `Add to ${playlist.name}`
+ break
+ case '-':
+ topBarButton.icon = ICON_MINUS
+ topBarButton.label = `Remove from ${playlist.name}`
+ break
+ case '?':
+ topBarButton.icon = ICON_LOADING
+ topBarButton.label = 'Loading...'
+ break
+ }
+}
+
+// ==============================================================================================================
+// ==============================================================================================================
+// ==============================================================================================================
+
+// Inject a new class used to highlight selected playlist
+function initInjectCss (): void {
+ const body = document.body
+ const style = document.createElement('style')
+
+ // ----------------------------------------------------------
+ // Avoid double injection
+
+ if (!body.classList.contains('quick-add-to-playlist--selected-playlist')) {
+ style.innerHTML = `
+ .quick-add-to-playlist--selected-playlist {
+ background-color: #323959 !important;
+ }
+ `
+
+ body.appendChild(style)
+ }
+
+ // ----------------------------------------------------------
+ // init selected playlist style
+
+ const playlist = getPlaylistFromLocalstorage()
+ if (playlist) updateStyle(null, playlist.id)
+
+ // ----------------------------------------------------------
+ // create an observer to watch for playlist folders wrapping
+
+ setTimeout(() => {
+ // Select the node that will be observed for mutations
+ const targetNode = document.querySelector('.main-rootlist-wrapper')
+ if (!targetNode) return
+
+ // Options for the observer (which mutations to observe)
+ const config = { attributes: true, childList: true, subtree: true }
+
+ // Callback function to execute when mutations are observed
+ const callback = (mutationList: any) => {
+ for (const mutation of mutationList) {
+ if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
+ const playlist = getPlaylistFromLocalstorage()
+ if (playlist) updateStyle(null, playlist.id)
+ }
+ }
+ }
+
+ // Create an observer instance linked to the callback function
+ const observer = new MutationObserver(callback)
+ observer.observe(targetNode, config)
+ }, 2000)
+}
+
+// Add/remove class to playlist's div container
+function updateStyle (oldPlaylistId: string | null, newPlaylistId: string | null): void {
+ const oldPlaylistLink = document.querySelector(`a[href="/playlist/${oldPlaylistId}"`)
+ oldPlaylistLink?.parentElement?.classList.remove('quick-add-to-playlist--selected-playlist')
+
+ const newPlaylistLink = document.querySelector(`a[href="/playlist/${newPlaylistId}"`)
+ newPlaylistLink?.parentElement?.classList.add('quick-add-to-playlist--selected-playlist')
+}
+
+// ==============================================================================================================
+// ==============================================================================================================
+// ==============================================================================================================
+
+function getPlaylistFromLocalstorage () {
+ return JSON.parse(Spicetify.LocalStorage.get(LOCALSTORAGE_PLAYLIST_KEY) as string)
+}
+
+export default main
diff --git a/src/settings.json b/src/settings.json
new file mode 100644
index 0000000..fce3ad7
--- /dev/null
+++ b/src/settings.json
@@ -0,0 +1,3 @@
+{
+ "nameId": "quick-add-to-playlist"
+}
\ No newline at end of file
diff --git a/src/types/css-modules.d.ts b/src/types/css-modules.d.ts
new file mode 100644
index 0000000..8c454a3
--- /dev/null
+++ b/src/types/css-modules.d.ts
@@ -0,0 +1,9 @@
+declare module '*.module.css' {
+ const classes: { [key: string]: string };
+ export default classes;
+}
+
+declare module '*.module.scss' {
+ const classes: { [key: string]: string };
+ export default classes;
+}
diff --git a/src/types/spicetify.d.ts b/src/types/spicetify.d.ts
new file mode 100644
index 0000000..c5d579a
--- /dev/null
+++ b/src/types/spicetify.d.ts
@@ -0,0 +1,1364 @@
+declare namespace Spicetify {
+ type Metadata = Partial>;
+ type ContextTrack = {
+ uri: string;
+ uid?: string;
+ metadata?: Metadata;
+ };
+ type ProvidedTrack = ContextTrack & {
+ removed?: string[];
+ blocked?: string[];
+ provider?: string;
+ };
+ interface 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;
+ };
+ 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;
+ };
+ 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
+ */
+ path: string;
+ /**
+ * 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
+ */
+ async function playUri(uri: string, context: any = {}, options: Options = {});
+ /**
+ * 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/album or array of tracks/albums to prioritized queue.
+ */
+ function addToQueue(uri: string | string[]): 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;
+ }
+
+ type Headers = Record;
+ type Body = Record;
+
+ interface Response {
+ body: any;
+ headers: Headers;
+ status: number;
+ uri: string;
+ static isSuccessStatus(status: number): boolean;
+ }
+
+ 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, 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);
+ function registerIsolatedShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void);
+ function registerImportantShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void);
+ function _deregisterShortcut(keys: KeysDefine);
+ function deregisterImportantShortcut(keys: KeysDefine);
+ };
+
+ /**
+ * @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;
+ }
+ /**
+ * 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);
+ 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;
+ /**
+ * Item is only available in Profile menu when method "register" is called.
+ */
+ register(): void;
+ /**
+ * Stop item to be prepended into Profile menu.
+ */
+ deregister(): 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/album or array of tracks/albums from current queue.
+ */
+ function removeFromQueue(uri: string | string[]): Promise;
+ /**
+ * Display a bubble of notification. Useful for a visual feedback.
+ */
+ function showNotification(text: string): void;
+ /**
+ * Set of APIs method to parse and validate URIs.
+ */
+ class URI {
+ constructor(type: string, props: any);
+ public type: string;
+ public id: string;
+
+ /**
+ * Creates an application URI object from the current URI object.
+ *
+ * If the current URI object is already an application type, a copy is made.
+ *
+ * @return The current URI as an application URI.
+ */
+ toAppType(): URI;
+
+ /**
+ * Creates a URI object from an application URI object.
+ *
+ * If the current URI object is not an application type, a copy is made.
+ *
+ * @return The current URI as a real typed URI.
+ */
+ toRealType(): URI;
+
+ /**
+ *
+ * @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;
+
+ /**
+ *
+ * @return The Play URL string for the uri.
+ */
+ toPlayURL(): string;
+
+ /**
+ *
+ * @return The URL string for the uri.
+ */
+ toURL(): string;
+
+ /**
+ *
+ * @return The Open URL string for the uri.
+ */
+ toOpenURL(): string;
+
+ /**
+ *
+ * @return The Play HTTPS URL string for the uri.
+ */
+ toSecurePlayURL(): string;
+
+ /**
+ *
+ * @return The HTTPS URL string for the uri.
+ */
+ toSecureURL(): string;
+
+ /**
+ *
+ * @return The Open HTTPS URL string for the uri.
+ */
+ toSecureOpenURL(): string;
+
+ /**
+ *
+ * @return The id of the uri as a bytestring.
+ */
+ idToByteString(): string;
+
+ getPath(): string;
+
+ getBase62Id(): string;
+
+ /**
+ * 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 uri The uri to compare identity for.
+ * @return Whether they shared idenitity
+ */
+ isSameIdentity(uri: any): boolean;
+
+ /**
+ * 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: {
+ EMPTY: string;
+ ALBUM: string;
+ AD: string;
+ /** URI particle; not an actual URI. */
+ APP: string;
+ APPLICATION: string;
+ ARTIST: string;
+ ARTIST_TOPLIST: string;
+ AUDIO_FILE: string;
+ COLLECTION: string;
+ COLLECTION_ALBUM: string;
+ COLLECTION_MISSING_ALBUM: string;
+ COLLECTION_ARTIST: string;
+ CONTEXT_GROUP: string;
+ DAILY_MIX: string;
+ EPISODE: string;
+ /** URI particle; not an actual URI. */
+ FACEBOOK: string;
+ FOLDER: string;
+ FOLLOWERS: string;
+ FOLLOWING: string;
+ /** URI particle; not an actual URI. */
+ GLOBAL: string;
+ IMAGE: string;
+ INBOX: string;
+ INTERRUPTION: string;
+ LOCAL_ARTIST: string;
+ LOCAL_ALBUM: string;
+ LOCAL: string;
+ LIBRARY: string;
+ MOSAIC: string;
+ PLAYLIST: string;
+ /** Only used for URI classification. Not a valid URI fragment. */
+ PLAYLIST_V2: string;
+ PROFILE: string;
+ PUBLISHED_ROOTLIST: string;
+ RADIO: string;
+ ROOTLIST: string;
+ COLLECTION_TRACK_LIST: string;
+ SEARCH: string;
+ SHOW: string;
+ SOCIAL_SESSION: string,
+ CONCERT: string;
+ SPECIAL: string;
+ STARRED: string;
+ STATION: string;
+ TEMP_PLAYLIST: string;
+ /** URI particle; not an actual URI. */
+ TOP: string;
+ TOPLIST: string;
+ TRACK: string;
+ TRACKSET: string;
+ /** URI particle; not an actual URI. */
+ USER: string;
+ USER_TOPLIST: string;
+ USER_TOP_TRACKS: 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;
+
+ /**
+ * 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;
+
+ /**
+ * Creates a new URI from a bytestring.
+ *
+ * @param type The type of the URI.
+ * @param idByteString The ID of the URI as a bytestring.
+ * @param opt_args Optional arguments to the URI constructor.
+ * @return The URI object created.
+ */
+ static fromByteString(type: string, idByteString: string, opt_args?: any): URI;
+
+ /**
+ * Clones a given SpotifyURI instance.
+ *
+ * @param uri The uri to clone.
+ * @return An instance of URI.
+ */
+ static clone(uri: URI): URI | null;
+
+ /**
+ * Returns the canonical representation of a username.
+ *
+ * @param username The username to encode.
+ * @return The encoded canonical representation of the username.
+ */
+ static getCanonicalUsername(username: string): string;
+
+ /**
+ * Returns the non-canonical representation of a username.
+ *
+ * @param username The username to encode.
+ * @return The unencoded canonical representation of the username.
+ */
+ static getDisplayUsername(username: string): string;
+
+ /**
+ * 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 empty URI.
+ *
+ * @return The empty URI.
+ */
+ static emptyURI(): URI;
+
+ /**
+ * 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 'ad' type URI.
+ *
+ * @param id The id of the ad.
+ * @return The ad URI.
+ */
+ static adURI(id: string): URI;
+
+ /**
+ * Creates a new 'audiofile' type URI.
+ *
+ * @param extension The extension of the audiofile.
+ * @param id The id of the extension.
+ * @return The audiofile URI.
+ */
+ static audioFileURI(extension: string, id: 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 'artist-toplist' type URI.
+ *
+ * @param id The id of the artist.
+ * @param toplist The toplist type.
+ * @return The artist-toplist URI.
+ */
+ static artistToplistURI(id: string, toplist: string): URI;
+
+ /**
+ * Creates a new 'dailymix' type URI.
+ *
+ * @param args An array of arguments for the dailymix.
+ * @return The dailymix URI.
+ */
+ static dailyMixURI(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 '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 'trackset' type URI.
+ *
+ * @param tracks An array of 'track' type URIs.
+ * @param name The name of the trackset.
+ * @param index The index in the trackset.
+ * @return The trackset URI.
+ */
+ static tracksetURI(tracks: URI[], name: string, index: number): URI;
+
+ /**
+ * Creates a new 'facebook' type URI.
+ *
+ * @param uid The user id.
+ * @return The facebook URI.
+ */
+ static facebookURI(uid: string): URI;
+
+ /**
+ * Creates a new 'followers' type URI.
+ *
+ * @param username The non-canonical username.
+ * @return The followers URI.
+ */
+ static followersURI(username: string): URI;
+
+ /**
+ * Creates a new 'following' type URI.
+ *
+ * @param username The non-canonical username.
+ * @return The following URI.
+ */
+ static followingURI(username: string): URI;
+
+ /**
+ * Creates a new 'playlist' type URI.
+ *
+ * @param username The non-canonical username of the playlist owner.
+ * @param id The id of the playlist.
+ * @return The playlist URI.
+ */
+ static playlistURI(username: string, id: 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 'folder' type URI.
+ *
+ * @param username The non-canonical username of the folder owner.
+ * @param id The id of the folder.
+ * @return The folder URI.
+ */
+ static folderURI(username: string, id: string): URI;
+
+ /**
+ * Creates a new 'collectiontracklist' type URI.
+ *
+ * @param username The non-canonical username of the collection owner.
+ * @param id The id of the tracklist.
+ * @return The collectiontracklist URI.
+ */
+ static collectionTrackList(username: string, id: string): URI;
+
+ /**
+ * Creates a new 'starred' type URI.
+ *
+ * @param username The non-canonical username of the starred list owner.
+ * @return The starred URI.
+ */
+ static starredURI(username: string): 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;
+
+ /**
+ * Creates a new 'user-top-tracks' type URI.
+ *
+ * @deprecated
+ * @param username The non-canonical username of the toplist owner.
+ * @return The user-top-tracks URI.
+ */
+ static userTopTracksURI(username: string): URI;
+
+ /**
+ * Creates a new 'toplist' type URI.
+ *
+ * @param toplist The toplist type.
+ * @param country The country code for the toplist.
+ * @param global True if this is a global rather than a country list.
+ * @return The toplist URI.
+ */
+ static toplistURI(toplist: string, country: string, global: boolean): URI;
+
+ /**
+ * Creates a new 'inbox' type URI.
+ *
+ * @param username The non-canonical username of the inbox owner.
+ * @return The inbox URI.
+ */
+ static inboxURI(username: string): URI;
+
+ /**
+ * Creates a new 'rootlist' type URI.
+ *
+ * @param username The non-canonical username of the rootlist owner.
+ * @return The rootlist URI.
+ */
+ static rootlistURI(username: string): URI;
+
+ /**
+ * Creates a new 'published-rootlist' type URI.
+ *
+ * @param username The non-canonical username of the published-rootlist owner.
+ * @return The published-rootlist URI.
+ */
+ static publishedRootlistURI(username: string): URI;
+
+ /**
+ * Creates a new 'local-artist' type URI.
+ *
+ * @param artist The artist name.
+ * @return The local-artist URI.
+ */
+ static localArtistURI(artist: string): URI;
+
+ /**
+ * Creates a new 'local-album' type URI.
+ *
+ * @param artist The artist name.
+ * @param album The album name.
+ * @return The local-album URI.
+ */
+ static localAlbumURI(artist: string, album: string): URI;
+
+ /**
+ * Creates a new 'local' type URI.
+ *
+ * @param artist The artist name.
+ * @param album The album name.
+ * @param track The track name.
+ * @param duration The track duration in ms.
+ * @return The local URI.
+ */
+ static localURI(artist: string, album: string, track: string, duration: number): URI;
+
+ /**
+ * Creates a new 'library' type URI.
+ *
+ * @param username The non-canonical username of the rootlist owner.
+ * @param category The category of the library.
+ * @return The library URI.
+ */
+ static libraryURI(username: string, category: 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 'temp-playlist' type URI.
+ *
+ * @param origin The origin of the temporary playlist.
+ * @param data Additional data for the playlist.
+ * @return The temp-playlist URI.
+ */
+ static temporaryPlaylistURI(origin: string, data: string): URI;
+
+ /**
+ * Creates a new 'context-group' type URI.
+ *
+ * @deprecated
+ * @param origin The origin of the temporary playlist.
+ * @param name The name of the context group.
+ * @return The context-group URI.
+ */
+ static contextGroupURI(origin: string, name: 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 'image' type URI.
+ *
+ * @param id The id of the image.
+ * @return The image URI.
+ */
+ static imageURI(id: string): URI;
+
+
+ /**
+ * Creates a new 'mosaic' type URI.
+ *
+ * @param ids The ids of the mosaic immages.
+ * @return The mosaic URI.
+ */
+ static mosaicURI(ids: string[]): URI;
+
+ /**
+ * Creates a new 'radio' type URI.
+ *
+ * @param args The radio seed arguments.
+ * @return The radio URI.
+ */
+ static radioURI(args: string): URI;
+
+ /**
+ * Creates a new 'special' type URI.
+ *
+ * @param args An array containing the other arguments.
+ * @return The special URI.
+ */
+ static specialURI(args: 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 '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 '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-album-missing' type URI.
+ *
+ * @param username The non-canonical username of the rootlist owner.
+ * @param id The id of the album.
+ * @return The collection-album-missing URI.
+ */
+ static collectionMissingAlbumURI(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 collectionArtistURI(username: string, id: string): URI;
+
+ /**
+ * Creates a new 'episode' type URI.
+ *
+ * @param id The id of the episode.
+ * @param context An optional context URI
+ * @param play Toggles autoplay in the episode URI
+ * @return The episode URI.
+ */
+ static episodeURI(id: string, context: string, play: boolean): 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 'concert' type URI.
+ *
+ * @param id The id of the concert.
+ * @return The concert URI.
+ */
+ static concertURI(id: string): URI;
+
+ /**
+ * Creates a new 'socialsession' type URI.
+ *
+ * @param id The token needed to join a social session.
+ * @return The socialsession URI.
+ */
+ static socialSessionURI(id: string): URI;
+
+ /**
+ * Creates a new 'interruption' type URI.
+ *
+ * @param id The id of the interruption.
+ * @return The ad URI.
+ */
+ static interruptionURI(id: string): URI;
+
+ static isAlbum(uri: any): boolean;
+ static isAd(uri: any): boolean;
+ static isApplication(uri: any): boolean;
+ static isArtist(uri: any): boolean;
+ static isCollection(uri: any): boolean;
+ static isCollectionAlbum(uri: any): boolean;
+ static isCollectionArtist(uri: any): boolean;
+ static isDailyMix(uri: any): boolean;
+ static isEpisode(uri: any): boolean;
+ static isFacebook(uri: any): boolean;
+ static isFolder(uri: any): boolean;
+ static isLocalArtist(uri: any): boolean;
+ static isLocalAlbum(uri: any): boolean;
+ static isLocalTrack(uri: any): boolean;
+ static isMosaic(uri: any): boolean;
+ static isPlaylistV1(uri: any): boolean;
+ static isPlaylistV2(uri: any): boolean;
+ static isRadio(uri: any): boolean;
+ static isRootlist(uri: any): boolean;
+ static isSearch(uri: any): boolean;
+ static isShow(uri: any): boolean;
+ static isConcert(uri: any): boolean;
+ static isStation(uri: any): boolean;
+ static isTrack(uri: any): boolean;
+ static isProfile(uri: any): boolean;
+ static isPlaylistV1OrV2(uri: any): boolean;
+ static isSocialSession(uri: any): boolean;
+ static isInterruption(uri: any): boolean;
+ }
+
+ /**
+ * Create custom menu item and prepend to right click context menu
+ */
+ namespace ContextMenu {
+ type Icon = "album" | "artist" | "block" | "chart-down" | "chart-up" | "check" | "check-alt-fill" | "chevron-left" | "chevron-right" | "chromecast-disconnected" | "copy" | "download" | "downloaded" | "edit" | "exclamation-circle" | "external-link" | "facebook" | "follow" | "fullscreen" | "grid-view" | "heart" | "heart-active" | "instagram" | "list-view" | "locked" | "locked-active" | "lyrics" | "minimize" | "more" | "new-spotify-connect" | "offline" | "pause" | "play" | "playlist" | "playlist-folder" | "plus2px" | "plus-alt" | "podcasts" | "repeat" | "repeat-once" | "search" | "search-active" | "shuffle" | "skip-back" | "skip-back15" | "skip-forward" | "skip-forward15" | "soundbetter" | "subtitles" | "twitter" | "volume" | "volume-off" | "volume-one-wave" | "volume-two-wave" | "x";
+ 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ function display(e: Content): void;
+ function hide(): void;
+ }
+
+ /** React instance to create components */
+ const React: any;
+ /** React DOM instance to render and mount components */
+ const ReactDOM: any;
+
+ /** 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: Spicetify.ReactComponent.Menu |
+ Spicetify.ReactComponent.AlbumMenu |
+ Spicetify.ReactComponent.PodcastShowMenu |
+ Spicetify.ReactComponent.ArtistMenu |
+ Spicetify.ReactComponent.PlaylistMenu;
+ /**
+ * A child of the context menu. Should be `