1
0
Fork 0

Add dot_config/spicetify/CustomApps/spicetify-marketplace-tmp/.keep

Add dot_config/spicetify/CustomApps/spicetify-marketplace/CONTRIBUTING.md
Add dot_config/spicetify/CustomApps/spicetify-marketplace/LICENSE
Add dot_config/spicetify/CustomApps/spicetify-marketplace/README.md
Add dot_config/spicetify/CustomApps/spicetify-marketplace/blacklist.json
Add dot_config/spicetify/CustomApps/spicetify-marketplace/dot_eslintrc.json
Add dot_config/spicetify/CustomApps/spicetify-marketplace/dot_github/ISSUE_TEMPLATE/bug_report.yml
Add dot_config/spicetify/CustomApps/spicetify-marketplace/dot_github/ISSUE_TEMPLATE/config.yml
Add dot_config/spicetify/CustomApps/spicetify-marketplace/dot_github/ISSUE_TEMPLATE/feature_request.yml
Add dot_config/spicetify/CustomApps/spicetify-marketplace/dot_github/ISSUE_TEMPLATE/malicious_extension_report.yml
Add dot_config/spicetify/CustomApps/spicetify-marketplace/dot_github/dependabot.yml
Add dot_config/spicetify/CustomApps/spicetify-marketplace/dot_github/workflows/lint.yml
Add dot_config/spicetify/CustomApps/spicetify-marketplace/dot_gitignore
Add dot_config/spicetify/CustomApps/spicetify-marketplace/dot_nvmrc
Add dot_config/spicetify/CustomApps/spicetify-marketplace/executable_install.sh
Add dot_config/spicetify/CustomApps/spicetify-marketplace/extension.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/index.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/install.ps1
Add dot_config/spicetify/CustomApps/spicetify-marketplace/manifest.json
Add dot_config/spicetify/CustomApps/spicetify-marketplace/package-lock.json
Add dot_config/spicetify/CustomApps/spicetify-marketplace/package.json
Add dot_config/spicetify/CustomApps/spicetify-marketplace/snippets.json
Add dot_config/spicetify/CustomApps/spicetify-marketplace/src/AddSnippetModal.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/src/Card.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/src/Icons.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/src/OptionsMenu.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/src/ReadmePage.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/src/ReloadModal.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/src/Settings.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/src/SortBox.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/src/TabBar.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/src/Utils.js
Add dot_config/spicetify/CustomApps/spicetify-marketplace/style.css
Add dot_config/spicetify/CustomApps/spicetify-marketplace/styles/components/add-snippet-modal.scss
Add dot_config/spicetify/CustomApps/spicetify-marketplace/styles/components/card.scss
Add dot_config/spicetify/CustomApps/spicetify-marketplace/styles/components/fixes.scss
Add dot_config/spicetify/CustomApps/spicetify-marketplace/styles/components/grid.scss
Add dot_config/spicetify/CustomApps/spicetify-marketplace/styles/components/readme-pages.scss
Add dot_config/spicetify/CustomApps/spicetify-marketplace/styles/components/reload-modal.scss
Add dot_config/spicetify/CustomApps/spicetify-marketplace/styles/components/settings.scss
Add dot_config/spicetify/CustomApps/spicetify-marketplace/styles/style.scss
Add dot_config/spicetify/CustomApps/spicetify-marketplace/tsconfig.json
Add dot_config/spicetify/config-xpui.ini
Add dot_config/spicetify/private_Backup/private_executable_login.spa
Add dot_config/spicetify/private_Backup/private_executable_xpui.spa
Add dot_config/spicetify/private_Extensions/keyboardShortcut.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_executable_index.html
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_executable_login.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_executable_login.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_executable_manifest.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_executable_vendor~login.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_executable_vendor~login.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIAraOnly-Black.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIAraOnly-Bold.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIAraOnly-Book.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIAraOnly-Light.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUICyrOnly-Black.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUICyrOnly-Bold.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUICyrOnly-Book.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUICyrOnly-Light.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIHbrOnly-Black.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIHbrOnly-Bold.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIHbrOnly-Book.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIHbrOnly-Light.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIv3T-Black.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIv3T-Bold.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIv3T-Book.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_CircularSpUIv3T-Light.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_fonts/private_executable_spoticon_regular_2.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable___longest.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_af.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_am.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_ar.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_az.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_bg.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_bho.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_bn.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_cs.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_da.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_de.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_el.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_en.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_es-419.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_es.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_et.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_fa.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_fi.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_fil.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_fr-CA.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_fr.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_gu.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_he.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_hi.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_hr.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_hu.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_id.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_is.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_it.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_ja.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_kn.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_ko.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_lt.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_lv.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_ml.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_mr.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_ms.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_nb.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_ne.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_nl.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_or.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_pa-IN.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_pa-PK.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_pl.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_pt-BR.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_pt-PT.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_ro.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_ru.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_sk.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_sl.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_sr.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_sv.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_sw.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_ta.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_te.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_th.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_tr.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_uk.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_ur.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_vi.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_zh-CN.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_zh-TW.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_i18n/private_executable_zu.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_executable_background.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_executable_eye.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_executable_logo.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_executable_scrollbar-buttons.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_fake-web-player/private_executable_content-rtl.avif
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_fake-web-player/private_executable_content-small-rtl.avif
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_fake-web-player/private_executable_content-small.avif
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_fake-web-player/private_executable_content.avif
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_fake-web-player/private_executable_controls.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_fake-web-player/private_executable_footer-left.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_fake-web-player/private_executable_footer-right.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_fake-web-player/private_executable_side.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_social-buttons/private_executable_apple.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_social-buttons/private_executable_facebook.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_login/private_images/private_social-buttons/private_executable_google.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_12.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_185.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_188.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_188.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_258.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_258.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_375.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_375.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_381.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_396.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_448.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_479.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_479.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_696.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_774.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_774.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_818.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_818.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_blank.html
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_collection-music-download.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_in-app-messaging-preview.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_in-app-messaging-preview.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_index.html
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_licenses.html
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_manifest.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_vendor~xpui.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_vendor~xpui.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-desktop-modals.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-desktop-modals.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-desktop-routes-debug-utils.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-desktop-routes-debug-utils.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-desktop-routes-homepage-takeover-ad.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-desktop-routes-homepage-takeover-ad.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-album.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-all-episode-sponsors.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-all-episode-sponsors.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-all-show-sponsors.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-artist.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-artist.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-blend.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-blend.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-collection-concerts.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-collection-concerts.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-collection-local-files.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-collection-local-files.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-concert.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-concert.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-create-playlist.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-download-page.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-download-page.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-enhanced-playlist.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-enhanced-playlist.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-episode.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-episode.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-folder.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-folder.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-offline-browse.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-offline-browse.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-playlist.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-playlist.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-profile.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-profile.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-queue.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-queue.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-track-v2.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-track-v2.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-track.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-view.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui-routes-view.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui.css
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_executable_xpui.js
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIAraOnly-Black.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIAraOnly-Bold.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIAraOnly-Book.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIAraOnly-Light.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUICyrOnly-Black.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUICyrOnly-Bold.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUICyrOnly-Book.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUICyrOnly-Light.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIHbrOnly-Black.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIHbrOnly-Bold.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIHbrOnly-Book.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIHbrOnly-Light.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIv3T-Black.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIv3T-Bold.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIv3T-Book.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_fonts/private_executable_CircularSpUIv3T-Light.woff2
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable___longest.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_af.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_am.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_ar.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_az.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_bg.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_bho.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_bn.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_cs.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_da.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_de.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_el.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_en.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_es-419.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_es.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_et.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_fa.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_fi.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_fil.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_fr-CA.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_fr.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_gu.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_he.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_hi.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_hr.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_hu.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_id.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_is.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_it.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_ja.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_kn.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_ko.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_lt.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_lv.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_ml.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_mr.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_ms.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_nb.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_ne.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_nl.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_or.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_pa-IN.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_pa-PK.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_pl.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_pt-BR.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_pt-PT.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_ro.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_ru.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_sk.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_sl.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_sr.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_sv.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_sw.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_ta.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_te.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_th.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_tr.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_uk.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_ur.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_vi.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_zh-CN.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_zh-TW.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_i18n/private_executable_zu.json
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_devices/private_executable_mac.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_connect-to-devices-white.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_connect_header@1x.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_equaliser-animated-green.gif
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_equaliser-green.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_hifi-button-off.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_hifi-button-on.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_hifi.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_playlist-folder-filled.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_station-header-wave.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_station-wave.svg
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_sw_saber_ahsoka1.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_sw_saber_anakin.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_sw_saber_leia.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_sw_saber_luke.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_sw_saber_mace.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_sw_saber_rey.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_sw_saber_vader.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_xpui/private_images/private_executable_tracklist-placeholder.png
Add dot_config/spicetify/private_Extracted/private_Raw/private_zlink/private_executable_index.html
Add dot_config/spicetify/private_Extracted/private_Raw/private_zlink/private_executable_manifest.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_executable_index.html
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_executable_login.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_executable_login.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_executable_manifest.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_executable_vendor~login.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_executable_vendor~login.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_fonts/.keep
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable___longest.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_af.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_am.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_ar.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_az.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_bg.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_bho.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_bn.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_cs.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_da.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_de.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_el.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_en.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_es-419.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_es.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_et.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_fa.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_fi.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_fil.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_fr-CA.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_fr.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_gu.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_he.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_hi.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_hr.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_hu.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_id.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_is.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_it.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_ja.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_kn.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_ko.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_lt.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_lv.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_ml.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_mr.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_ms.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_nb.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_ne.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_nl.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_or.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_pa-IN.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_pa-PK.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_pl.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_pt-BR.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_pt-PT.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_ro.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_ru.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_sk.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_sl.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_sr.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_sv.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_sw.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_ta.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_te.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_th.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_tr.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_uk.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_ur.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_vi.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_zh-CN.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_zh-TW.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_i18n/private_executable_zu.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_images/private_fake-web-player/.keep
Add dot_config/spicetify/private_Extracted/private_Themed/private_login/private_images/private_social-buttons/.keep
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_12.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_185.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_188.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_188.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_258.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_258.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_375.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_375.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_381.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_396.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_448.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_479.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_479.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_696.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_774.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_774.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_818.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_818.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_blank.html
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_collection-music-download.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_in-app-messaging-preview.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_in-app-messaging-preview.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_index.html
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_licenses.html
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_manifest.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_vendor~xpui.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_vendor~xpui.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-desktop-modals.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-desktop-modals.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-desktop-routes-debug-utils.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-desktop-routes-debug-utils.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-desktop-routes-homepage-takeover-ad.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-desktop-routes-homepage-takeover-ad.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-album.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-all-episode-sponsors.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-all-episode-sponsors.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-all-show-sponsors.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-artist.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-artist.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-blend.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-blend.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-collection-concerts.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-collection-concerts.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-collection-local-files.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-collection-local-files.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-concert.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-concert.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-create-playlist.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-download-page.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-download-page.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-enhanced-playlist.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-enhanced-playlist.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-episode.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-episode.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-folder.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-folder.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-offline-browse.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-offline-browse.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-playlist.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-playlist.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-profile.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-profile.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-queue.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-queue.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-track-v2.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-track-v2.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-track.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-view.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui-routes-view.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui.css
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_executable_xpui.js
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_fonts/.keep
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable___longest.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_af.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_am.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_ar.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_az.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_bg.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_bho.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_bn.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_cs.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_da.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_de.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_el.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_en.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_es-419.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_es.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_et.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_fa.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_fi.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_fil.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_fr-CA.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_fr.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_gu.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_he.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_hi.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_hr.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_hu.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_id.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_is.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_it.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_ja.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_kn.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_ko.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_lt.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_lv.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_ml.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_mr.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_ms.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_nb.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_ne.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_nl.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_or.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_pa-IN.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_pa-PK.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_pl.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_pt-BR.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_pt-PT.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_ro.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_ru.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_sk.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_sl.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_sr.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_sv.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_sw.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_ta.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_te.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_th.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_tr.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_uk.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_ur.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_vi.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_zh-CN.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_zh-TW.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_i18n/private_executable_zu.json
Add dot_config/spicetify/private_Extracted/private_Themed/private_xpui/private_images/private_devices/.keep
Add dot_config/spicetify/private_Extracted/private_Themed/private_zlink/private_executable_index.html
Add dot_config/spicetify/private_Extracted/private_Themed/private_zlink/private_executable_manifest.json
Add dot_config/spicetify/private_Themes/.keep
This commit is contained in:
Florian RICHER 2022-05-07 17:45:10 +02:00
parent ebbd385eb3
commit f04977817c
529 changed files with 370636 additions and 0 deletions

View file

@ -0,0 +1,151 @@
<!-- omit in toc -->
# Contributing to Spicetify-Marketplace
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
> - Tweet about it
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
<!-- omit in toc -->
## Table of Contents
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team)
## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation]().
Before you ask a question, it is best to search for existing [Issues](https://github.com/CharlieS1103/spicetify-marketplaceissues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an [Issue](https://github.com/CharlieS1103/spicetify-marketplaceissues/new).
- Provide as much context as you can about what you're running into.
- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
We will then take care of the issue as soon as possible.
<!--
You might want to create a separate issue tag for questions and include it in this description. People should then tag their issues accordingly.
Depending on how large the project is, you may want to outsource the questioning, e.g. to Stack Overflow or Gitter. You may add additional contact and information possibilities:
- IRC
- Slack
- Gitter
- Stack Overflow tag
- Blog
- FAQ
- Roadmap
- E-Mail List
- Forum
-->
## I Want To Contribute
> ### Legal Notice <!-- omit in toc -->
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
### Reporting Bugs
<!-- omit in toc -->
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/CharlieS1103/spicetify-marketplaceissues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
<!-- omit in toc -->
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to <charlieS#1351 on Discord>.
<!-- You may add a PGP key to allow the messages to be sent encrypted as well. -->
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](https://github.com/CharlieS1103/spicetify-marketplaceissues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
<!-- You might want to create an issue template for bugs and errors that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for Spicetify-Marketplace, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
<!-- omit in toc -->
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation]() carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](https://github.com/CharlieS1103/spicetify-marketplaceissues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
<!-- omit in toc -->
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](https://github.com/CharlieS1103/spicetify-marketplaceissues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. <!-- this should only be included if the project has a GUI -->
- **Explain why this enhancement would be useful** to most Spicetify-Marketplace users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
<!-- You might want to create an issue template for enhancement suggestions that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
### Your First Code Contribution
<!-- TODO
include Setup of env, IDE and typical getting started instructions?
-->
### Improving The Documentation
<!-- TODO
Updating, improving and correcting the documentation
-->
## Styleguides
### Commit Messages
<!-- TODO
-->
## Join The Project Team
<!-- TODO -->
<!-- omit in toc -->
## Attribution
This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)!

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 CharlieS1103
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,178 @@
# spicetify-marketplace
Download extensions and themes directly from within [Spicetify](https://github.com/khanhas/spicetify-cli).
Made by [@theRealpadster](https://github.com/theRealPadster) and [@CharlieS1103](https://github.com/CharlieS1103).
Based on the [reddit Custom App](https://github.com/khanhas/spicetify-cli/wiki/Custom-Apps#reddit)
<p align="center">
<a href="https://github.com/CharlieS1103/spicetify-marketplace/releases/latest">
<img src="https://img.shields.io/github/v/release/CharlieS1103/spicetify-marketplace?include_prereleases">
</a>
<a href="https://github.com/CharlieS1103/spicetify-marketplace/issues?q=is%3Aissue+is%3Aclosed">
<img src="https://img.shields.io/github/issues-closed/CharlieS1103/spicetify-marketplace">
</a>
<a href="https://github.com/CharlieS1103/spicetify-marketplace/commits/main">
<img src="https://img.shields.io/github/commit-activity/m/CharlieS1103/spicetify-marketplace">
</a>
</p>
## Disclaimer
All extensions are from community. They might contain unwanted code. Be careful what you install, or review the code before use. We hold no responsibility for these extensions or anything installed via this custom app. If you find a malicious extension, please [submit an issue](https://github.com/CharlieS1103/spicetify-marketplace/issues/new?template=malicious-extension-report.md) and we can add it to the [blacklist](blacklist.json).
## Notes:
This project is a work-in-progress and is not finished, polished, or guaranteed to work. Use at your own risk.
## Install
### Auto Install (Windows)
Open Powershell and paste the following:
```powershell
Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/CharlieS1103/spicetify-marketplace/master/install.ps1" | Invoke-Expression
```
### Auto Install (MacOS and Linux)
Open a terminal and paste the following:
```shell
curl -fsSL https://raw.githubusercontent.com/CharlieS1103/spicetify-marketplace/main/install.sh | sh
```
### Manual Install (recommended)
Copy the `spicetify-marketplace` folder into your [Spicetify](https://github.com/khanhas/spicetify-cli) custom apps directory:
| **Platform** | **Path** |
|------------|------------------------------------------------------------------------------------------|
| **Linux** | `~/.config/spicetify/CustomApps/` or `$XDG_CONFIG_HOME/.config/spicetify/CustomApps/`|
| **MacOS** | `~/.config/spicetify/CustomApps/` or `$SPICETIFY_CONFIG/CustomApps/` |
| **Windows** | `%userprofile%/.spicetify/CustomApps/` |
After putting the marketplace folder into the correct custom apps folder, run the following command to enable it:
```
spicetify config custom_apps spicetify-marketplace
spicetify apply
```
Note: Using the `config` command to add the custom app will always append the file name to the existing custom apps list. It does not replace the whole key's value.
Or you can manually edit your `config-xpui.ini` file. Add your desired custom apps folder names in the `custom_apps` key, separated them by the | character.
Example:
```ini
[AdditionalOptions]
...
custom_apps = reddit | spicetify-marketplace
```
Then run:
```
spicetify apply
```
## Manual reset
If you lose access to the marketplace page, and therefore cannot access the setting, follow these steps:
- First ensure you have dev-tools enabled by running ``spicetify enable-devtool apply``
- Open up console by right clicking anywhere on Spotify (that isn't an element), and clicking inspect element.
- Click the console tab in the devtools window that appears, and run the following command(in console)
- ``Marketplace.reset()``
## Getting your extension/theme on Marketplace
In order to show up in the custom app, you'll need to make a public GitHub repo that meets these requirements:
* Have the matching **GitHub topic tag** ("**spicetify-extensions**" or "**spicetify-themes**")
* Have a valid **`manifest.json`** in the root folder (format outlined below)
### Extension manifests
* `name`: Your extension name
* `description`: Description for your extension
* `preview`: A path to your preview image. Must be relative to your project root.
* `main`: The filename for your extension's main js file. Must be relative to your project root.
* `readme`: The filename for your extension's README file. Must be relative to your project root.
* `branch`: Optional branch to specify. Will use default branch if none.
* `authors`: Optional array of authors with names and urls. Will use repo owner if none.
* `tags`: Optional array of tags to show along with the card.
e.g.
```json
[
{
"name": "Spicetify-Hide-Podcasts",
"description": "Spicetify extension to hide podcasts.",
"preview": "screenshot.png",
"main": "hidePodcasts.js",
"readme": "README.md",
"authors": [
{ "name": "theRealPadster", "url": "https://github.com/theRealPadster" }
],
"tags": ["podcasts"]
},
{
"name": "extensionName(No .js included)",
"description": "Another Spicetify extension to show how to make a manifest.",
"preview": "https://i.imgur.com/foo.png",
"main": "filepathFromGitRepo/myExt.js",
"readme": "filepathFromGitRepo/README.md",
"branch": "my-branch"
},
]
```
### Theme manifests
* `name`: Your theme name
* `description`: Description for your theme
* `preview`: A path to your preview image. Must be relative to your project root.
* `usercss`: A path to your user.css file. Must be relative to your project root.
* `schemes`: A path to your color.ini file. Must be relative to your project root.
* `readme`: The filename for your extension's README file. Must be relative to your project root.
* `branch`: Optional branch to specify. Will use default branch if none.
* `authors`: Optional array of authors with names and urls. Will use repo owner if none.
* `tags`: Optional array of tags to show along with the card.
e.g.
```json
{
"name": "Theme Name",
"description": "Theme description",
"preview": "filepathFromGitRepo/theme.png",
"readme": "README.md",
"usercss": "filepathFromGitRepo/user.css",
"schemes": "filepathFromGitRepo/color.ini",
"branch": "beta-release",
"authors": [
{ "name": "theRealPadster", "url": "https://github.com/theRealPadster" },
{ "name": "CharlieS1103", "url": "https://github.com/CharlieS1103" }
],
"tags": ["dark", "minimal"]
}
```
### Further manifest notes
* If you have multiple extensions in the same repo (such as using subfolders), you can make your `manifest.json` an array, and include them all.
* Most fields also support http/https URLs (`preview`, `main`, `readme`, `usercss`, `schemes`)
* If all your extensions/themes are in the root folder, you don't need to include an absolute file path.
## Snippets
CSS snippets are rather basic to implement. We fetch them from this repo, so you'll need to submit a [pull request](https://github.com/CharlieS1103/spicetify-marketplace/compare). In order to be valid JSON, the CSS needs to be in one line. You can use [this website](https://tools.knowledgewalls.com/online-multiline-to-single-line-converter) to make the css snippet single line. Once you have your code segment ready, edit snippets.json and add the following, before submitting your PR.
```json
{
"title": "Title",
"description": "description",
"code": "The single line css you have"
}
```
## Basic card loading functionality outline
1. `componentDidMount` triggers `newRequest`, which triggers `loadAmount(30)`
2. `loadAmount` calls `loadPage` in a loop until it has the requested amount of cards or runs out of results
3. `loadPage` calls `getRepos(page)` to get the next page of extensions. It queries the GitHub API for any repos with the "spicetify-extensions" topic. We'll likely add our own tag in the future, like "spicetify-marketplace".
4. The it loops through all the results and runs `fetchRepoExtensions()` or `getThemeRepos()`, which fetches a `manifest.json` file from the repo's root folder. If it finds one, we generate a card based on the info.
* Or if the active tab is "Installed", `loadPage` calls `getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedSnippets)` to get the extensions from the localstorage and generate the cards from there.
* Or if the active tab is "Snippets", `loadPage` calls `fetchCssSnippets()` and generates cards from the most recent `snippets.json` on GitHub.
## Styling + Build Process
- The stylesheet is built using Sass (scss) with the [Parcel](https://parceljs.org/) bundler
- The main stylesheet is style/style.scss, which builds all the components, and compiles into style.css
- For development, you can run `npm run watch` to live update the files as you save them
- For building, you can run `npm run build` to just build the style.css file once

View file

@ -0,0 +1,5 @@
{
"repos": [
"https://github.com/Darkempire78/Spotify-Customizer"
]
}

View file

@ -0,0 +1,102 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12
},
"plugins": [
"react"
],
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double",
{ "allowTemplateLiterals": true }
],
"semi": [
"error",
"always"
],
"comma-dangle": [
"error",
"always-multiline"
],
"no-var": "error",
"space-before-blocks": "error",
"comma-spacing": [
"error", { "before": false, "after": true }
],
"no-trailing-spaces": "error",
"keyword-spacing": "error",
"no-multiple-empty-lines": [
"error", { "max": 1 }
],
"object-curly-spacing": [
"error", "always"
],
"key-spacing": [
"error", { "beforeColon": false, "afterColon": true }
]
},
"settings": {
"react": {
"version": "detect"
}
},
"globals": {
"Card": "readonly",
"OptionsMenu": "readonly",
"ReadmePage": "readonly",
"SortBox": "readonly",
"TopBarContent": "readonly",
"react": "readonly",
"reactDOM": "readonly",
"Spicetify": "readonly",
"useEffect": "readonly",
"useState": "readonly",
"LOCALSTORAGE_KEYS": "writable",
"ALL_TABS": "readonly",
"CONFIG": "writable",
"gridUpdateTabs": "writable",
"gridUpdatePostsVisual": "writable",
"openConfig": "writable",
"openReloadModal": "writable",
"openAddSnippetModal": "writable",
"sortConfig": "writable",
"hexToRGB": "readonly",
"parseIni": "readonly",
"initializeSnippets": "writable",
"getLocalStorageDataFromKey": "writable",
"processAuthors": "readonly",
"generateSchemesOptions": "readonly",
"getBlacklist": "readonly",
"fetchCssSnippets": "readonly",
"resetMarketplace": "readonly",
"LoadingIcon": "readonly",
"LoadMoreIcon": "readonly",
"DOWNLOAD_ICON": "readonly",
"SETTINGS_ICON": "readonly",
"TRASH_ICON": "readonly"
}
}

View file

@ -0,0 +1,36 @@
name: 🐛 Bug report
description: Report errors or unexpected behavior
labels: [🐛 bug]
body:
- type: checkboxes
attributes:
label: 🔍 Is there already an issue for your problem?
description: Please make sure you are not creating an already submitted <a href="https://github.com/spicetify/spicetify-marketplace/issues">Issue</a>. Check closed issues as well, because your issue may have already been fixed.
options:
- label: I have checked older issues, open and closed
required: true
- type: textarea
attributes:
label: Environment / Computer Info
description: Please provide the details of the system Spicetify is running on.
value: |
- Spotify version:
- Spicetify version:
placeholder: |
- Spotify version: Spotify for macOS (Apple Silicon) 1.1.77.643.g3c4c6fc6
- Spicetify version: 2.8.5
render: Markdown
validations:
required: true
- type: textarea
attributes:
label: 📝 Description
description: List steps to reproduce the error and details on what happens and what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: 📸 Screenshots
description: Place any screenshots of the issue here if needed
validations:
required: false

View file

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: ✏️ Spicetify Docs
url: https://spicetify.app/
about: Check out our documentation here.

View file

@ -0,0 +1,21 @@
name: ✨ Feature Request
description: Request a new feature or enhancement
labels: [✨ feature]
body:
- type: textarea
id: description
attributes:
label: 📝 Provide a description of the new feature
description: What is the expected behavior of the proposed feature? What is the scenario this would be used?
validations:
required: true
- type: textarea
id: additional-information
attributes:
label: Additional Information
description: Give us some additional information on the feature request like proposed solutions, links, screenshots, etc.
- type: markdown
attributes:
value: If you'd like to see this feature implemented, add a 👍 reaction to this post.

View file

@ -0,0 +1,24 @@
name: 🦠 Malicious extension report
description: Submit any malicious extensions and we can add them to the denylist.
labels: [🦠 malicious extension]
body:
- type: input
id: extension-link
attributes:
label: 🔗 Extension Link
description: A link to the GitHub repo of the extension in question.
validations:
required: true
- type: textarea
id: extension-description
attributes:
label: 📝 Describe the Issue
description: What does the extension do that is unsafe/malicious?
validations:
required: true
- type: textarea
attributes:
label: 📸 Screenshots
description: If applicable, add screenshots to help explain your problem.
validations:
required: false

View file

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
ignore:
# ignore all patch updates
- dependency-name: "*"
update-types: ["version-update:semver-patch"]

View file

@ -0,0 +1,34 @@
name: Lint
on:
# Trigger the workflow on push or pull request,
# but only for the main branch
push:
branches:
- main
pull_request:
branches:
- main
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 16
# ESLint must be in `package.json`
- name: Install Node.js dependencies
run: npm ci
- name: Run linters
uses: wearerequired/lint-action@v1
with:
eslint: true

View file

@ -0,0 +1,5 @@
node_modules/
dump.rdb
.DS_Store
.parcel-cache/
*.map

View file

@ -0,0 +1 @@
16.10.0

View file

@ -0,0 +1,55 @@
#!/bin/sh
# Copyright 2019 khanhas. GPL license.
# Edited from project Denoland install script (https://github.com/denoland/deno_install)
set -e
latest_release_uri="https://github.com/CharlieS1103/spicetify-marketplace/archive/refs/heads/main.zip"
echo "DOWNLOADING $latest_release_uri"
download_uri="https://github.com/CharlieS1103/spicetify-marketplace/archive/refs/heads/main.zip"
SPICETIFY_CONFIG_DIR="${SPICETIFY_CONFIG:-$HOME/.config/spicetify}"
INSTALL_DIR="$SPICETIFY_CONFIG_DIR/CustomApps"
if [ ! -d "$INSTALL_DIR" ]; then
echo "MAKING FOLDER $INSTALL_DIR";
mkdir -p "$INSTALL_DIR"
fi
TAR_FILE="$INSTALL_DIR/spicetify-marketplace-main.zip"
echo "DOWNLOADING $download_uri"
curl --fail --location --progress-bar --output "$TAR_FILE" "$download_uri"
cd "$INSTALL_DIR"
echo "EXTRACTING"
unzip -q -d "$INSTALL_DIR/spicetify-marketplace-tmp" -o "$TAR_FILE"
echo "REMOVING"
rm "$TAR_FILE"
cd "$INSTALL_DIR/spicetify-marketplace-tmp"
# Check ~\.spicetify.\CustomApps directory already exists
# sp_dot_dir="$(dirname "$(../../spicetify -c)")/CustomApps/spicetify-marketplace"
# if [ ! -d "$sp_dot_dir" ]; then
# echo "MAKING FOLDER $sp_dot_dir";
# mkdir -p "$sp_dot_dir"
# fi
echo "COPYING"
# echo "$sp_dot_dir"
rm -rf "$INSTALL_DIR/spicetify-marketplace/"
mv "$INSTALL_DIR/spicetify-marketplace-tmp/spicetify-marketplace-main" "$INSTALL_DIR/spicetify-marketplace"
# cp -rf "$INSTALL_DIR/spicetify-marketplace/." "$sp_dot_dir"
echo "INSTALLING"
cd "$INSTALL_DIR/spicetify-marketplace"
if spicetify config custom_apps spicetify-marketplace ; then
echo "Added to config!"
echo "APPLYING"
spicetify apply
else
echo "Command failed"
echo "Please run \`spicetify config custom_apps spicetify-marketplace\` manually "
echo "Next run \`spicetify apply\`"
fi

View file

@ -0,0 +1,496 @@
// @ts-check
// NAME: Spicetify Marketplace Extension
// AUTHOR: theRealPadster, CharlieS1103
// DESCRIPTION: Companion extension for Spicetify Marketplace
/// <reference path="../spicetify-cli/globals.d.ts" />
// Reset any Marketplace localStorage keys (effectively resetting it completely)
// eslint-disable-next-line no-redeclare
const resetMarketplace = () => {
console.log("Resetting Marketplace");
// Loop through and reset marketplace keys
Object.keys(localStorage).forEach((key) => {
if (key.startsWith("marketplace:")) {
localStorage.removeItem(key);
console.log(`Removed ${key}`);
}
});
console.log("Marketplace has been reset");
location.reload();
};
// Expose useful methods in global context
// @ts-ignore
window.Marketplace = {
// Should allow you to reset Marketplace from the dev console if it's b0rked
reset: resetMarketplace,
};
// eslint-disable-next-line no-redeclare
const hexToRGB = (hex) => {
if (hex.length === 3) {
hex = hex.split("").map((char) => char + char).join("");
} else if (hex.length != 6) {
throw "Only 3- or 6-digit hex colours are allowed.";
} else if (hex.match(/[^0-9a-f]/i)) {
throw "Only hex colours are allowed.";
}
const aRgbHex = hex.match(/.{1,2}/g);
const aRgb = [
parseInt(aRgbHex[0], 16),
parseInt(aRgbHex[1], 16),
parseInt(aRgbHex[2], 16),
];
return aRgb;
};
/**
* Get user, repo, and branch from a GitHub raw URL
* @param {string} url Github Raw URL
* @returns { { user: string, repo: string, branch: string, filePath: string } }
*/
const getParamsFromGithubRaw = (url) => {
// eslint-disable-next-line no-useless-escape
const regex_result = url.match(/https:\/\/raw\.githubusercontent\.com\/(?<user>[^\/]+)\/(?<repo>[^\/]+)\/(?<branch>[^\/]+)\/(?<filePath>.+$)/);
// e.g. https://raw.githubusercontent.com/CharlieS1103/spicetify-extensions/main/featureshuffle/featureshuffle.js
const obj = {
user: regex_result ? regex_result.groups.user : null,
repo: regex_result ? regex_result.groups.repo : null,
branch: regex_result ? regex_result.groups.branch : null,
filePath: regex_result ? regex_result.groups.filePath : null,
};
return obj;
};
(async function MarketplaceExtension() {
const { LocalStorage } = Spicetify;
if (!(LocalStorage)) {
// console.log('Not ready, waiting...');
setTimeout(MarketplaceExtension, 1000);
return;
}
// TODO: can we reference/require/import common files between extension and custom app?
const LOCALSTORAGE_KEYS = {
"installedExtensions": "marketplace:installed-extensions",
"installedSnippets": "marketplace:installed-snippets",
"installedThemes": "marketplace:installed-themes",
"activeTab": "marketplace:active-tab",
"tabs": "marketplace:tabs",
// Theme installed store the localsorage key of the theme (e.g. marketplace:installed:NYRI4/Comfy-spicetify/user.css)
"themeInstalled": "marketplace:theme-installed",
"colorShift": "marketplace:colorShift",
};
const getLocalStorageDataFromKey = (key, fallback) => {
return JSON.parse(localStorage.getItem(key)) ?? fallback;
};
const initializeExtension = (extensionKey) => {
const extensionManifest = getLocalStorageDataFromKey(extensionKey);
// Abort if no manifest found or no extension URL (i.e. a theme)
if (!extensionManifest || !extensionManifest.extensionURL) return;
console.log("Initializing extension: ", extensionManifest);
const script = document.createElement("script");
script.defer = true;
script.src = extensionManifest.extensionURL;
// If it's a github raw script, use jsdelivr
if (script.src.indexOf("raw.githubusercontent.com") > -1) {
const { user, repo, branch, filePath } = getParamsFromGithubRaw(extensionManifest.extensionURL);
if (!user || !repo || !branch || !filePath) return;
script.src = `https://cdn.jsdelivr.net/gh/${user}/${repo}@${branch}/${filePath}`;
}
script.src = `${script.src}?time=${Date.now()}`;
document.body.appendChild(script);
};
/**
* Loop through the snippets and add the contents of the code as a style tag in the DOM
* @param { { title: string; description: string; code: string;}[] } snippets The snippets to initialize
*/
// TODO: keep this in sync with the index.js file
const initializeSnippets = (snippets) => {
// Remove any existing marketplace snippets
const existingSnippets = document.querySelector("style.marketplaceSnippets");
if (existingSnippets) existingSnippets.remove();
const style = document.createElement("style");
const styleContent = snippets.reduce((accum, snippet) => {
accum += `/* ${snippet.title} - ${snippet.description} */\n`;
accum += `${snippet.code}\n`;
return accum;
}, "");
style.innerHTML = styleContent;
style.classList.add("marketplaceSnippets");
document.head.appendChild(style);
};
// NOTE: Keep in sync with index.js
const injectColourScheme = (scheme) => {
try {
// Remove any existing Spicetify scheme
const existingColorsCSS = document.querySelector("link[href='colors.css']");
if (existingColorsCSS) existingColorsCSS.remove();
// Remove any existing marketplace scheme
const existingMarketplaceSchemeCSS = document.querySelector("style.marketplaceCSS.marketplaceScheme");
if (existingMarketplaceSchemeCSS) existingMarketplaceSchemeCSS.remove();
// Add new marketplace scheme
const schemeTag = document.createElement("style");
schemeTag.classList.add("marketplaceCSS");
schemeTag.classList.add("marketplaceScheme");
// const theme = document.querySelector('#theme');
let injectStr = ":root {";
const themeIniKeys = Object.keys(scheme);
themeIniKeys.forEach((key) => {
injectStr += `--spice-${key}: #${scheme[key]};`;
injectStr += `--spice-rgb-${key}: ${hexToRGB(scheme[key])};`;
});
injectStr += "}";
schemeTag.innerHTML = injectStr;
document.head.appendChild(schemeTag);
} catch (error) {
console.warn(error);
}
};
/**
* Update the user.css in the DOM
* @param {string} userCSS The contents of the new user.css
*/
const injectUserCSS = (userCSS) => {
try {
// Remove any existing Spicetify user.css
const existingUserThemeCSS = document.querySelector("link[href='user.css']");
if (existingUserThemeCSS) existingUserThemeCSS.remove();
// Remove any existing marketplace scheme
const existingMarketplaceUserCSS = document.querySelector("style.marketplaceCSS.marketplaceUserCSS");
if (existingMarketplaceUserCSS) existingMarketplaceUserCSS.remove();
// Add new marketplace scheme
const userCssTag = document.createElement("style");
userCssTag.classList.add("marketplaceCSS");
userCssTag.classList.add("marketplaceUserCSS");
userCssTag.innerHTML = userCSS;
document.head.appendChild(userCssTag);
} catch (error) {
console.warn(error);
}
};
// I guess this is okay to not have an end condition on the interval
// because if they turn the setting on or off,
// closing the settings modal will reload the page
const initColorShiftLoop = (schemes) => {
let i = 0;
const NUM_SCHEMES = Object.keys(schemes).length;
setInterval(() => {
// Resets to zero when passes the last scheme
i = i % NUM_SCHEMES;
const style = document.createElement("style");
style.className = "colorShift-style";
style.innerHTML = `* {
transition-duration: 400ms;
}
main-type-bass {
transition-duration: unset !important;
}`;
document.body.appendChild(style);
injectColourScheme(Object.values(schemes)[i]);
i++;
style.remove();
}, 60 * 1000);
};
const parseCSS = async (themeManifest) => {
const userCssUrl = themeManifest.cssURL.indexOf("raw.githubusercontent.com") > -1
// TODO: this should probably be the URL stored in localstorage actually (i.e. put this url in localstorage)
? `https://cdn.jsdelivr.net/gh/${themeManifest.user}/${themeManifest.repo}@${themeManifest.branch}/${themeManifest.manifest.usercss}`
: themeManifest.cssURL;
// TODO: Make this more versatile
const assetsUrl = userCssUrl.replace("/user.css", "/assets/");
console.log("Parsing CSS: ", userCssUrl);
let css = await fetch(`${userCssUrl}?time=${Date.now()}`).then(res => res.text());
// console.log("Parsed CSS: ", css);
let urls = css.matchAll(/url\(['|"](?<path>.+?)['|"]\)/gm) || [];
for (const match of urls) {
const url = match.groups.path;
// console.log(url);
// If it's a relative URL, transform it to HTTP URL
if (!url.startsWith("http") && !url.startsWith("data")) {
const newUrl = assetsUrl + url.replace(/\.\//g, "");
css = css.replace(url, newUrl);
}
}
// console.log("New CSS: ", css);
return css;
};
const initializeTheme = async (themeKey) => {
const themeManifest = getLocalStorageDataFromKey(themeKey);
// Abort if no manifest found
if (!themeManifest) {
console.log("No theme manifest found");
return;
}
console.log("Initializing theme: ", themeManifest);
// Inject colour scheme if found
if (themeManifest.schemes) {
const activeScheme = themeManifest.schemes[themeManifest.activeScheme];
injectColourScheme(activeScheme);
if (localStorage.getItem(LOCALSTORAGE_KEYS.colorShift) === "true") {
initColorShiftLoop(themeManifest.schemes);
}
} else {
console.warn("No schemes found for theme");
}
// Remove default css
// TODO: what about if we remove the theme? Should we re-add the user.css/colors.css?
// const existingUserThemeCSS = document.querySelector("link[href='user.css']");
// if (existingUserThemeCSS) existingUserThemeCSS.remove();
// Remove any existing marketplace theme
const existingMarketplaceThemeCSS = document.querySelector("link.marketplaceCSS");
if (existingMarketplaceThemeCSS) existingMarketplaceThemeCSS.remove();
// Add theme css
const userCSS = await parseCSS(themeManifest);
injectUserCSS(userCSS);
// Inject any included js
if (themeManifest.include && themeManifest.include.length) {
// console.log("Including js", installedThemeData.include);
themeManifest.include.forEach((script) => {
const newScript = document.createElement("script");
let src = script;
// If it's a github raw script, use jsdelivr
if (script.indexOf("raw.githubusercontent.com") > -1) {
const { user, repo, branch, filePath } = getParamsFromGithubRaw(script);
src = `https://cdn.jsdelivr.net/gh/${user}/${repo}@${branch}/${filePath}`;
}
// console.log({src});
newScript.src = `${src}?time=${Date.now()}`;
newScript.classList.add("marketplaceScript");
document.body.appendChild(newScript);
});
}
};
console.log("Loaded Marketplace extension");
const installedThemeKey = LocalStorage.get(LOCALSTORAGE_KEYS.themeInstalled);
if (installedThemeKey) initializeTheme(installedThemeKey);
const installedSnippetKeys = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedSnippets, []);
const installedSnippets = installedSnippetKeys.map((key) => getLocalStorageDataFromKey(key));
initializeSnippets(installedSnippets);
const installedExtensions = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedExtensions, []);
installedExtensions.forEach((extensionKey) => initializeExtension(extensionKey));
})();
const ITEMS_PER_REQUEST = 100;
async function Blacklist() {
const url = "https://raw.githubusercontent.com/CharlieS1103/spicetify-marketplace/main/blacklist.json";
const jsonReturned = await fetch(url).then(res => res.json()).catch(() => {});
return jsonReturned.repos;
}
/**
* TODO
* @param {"theme"|"extension"} type The repo type
* @param {number} pageNum The page number
* @returns TODO
*/
async function queryRepos(type, pageNum = 1) {
const BLACKLIST = window.sessionStorage.getItem("marketplace:blacklist");
let url = `https://api.github.com/search/repositories?per_page=${ITEMS_PER_REQUEST}`;
if (type === "extension") url += `&q=${encodeURIComponent("topic:spicetify-extensions")}`;
else if (type === "theme") url += `&q=${encodeURIComponent("topic:spicetify-themes")}`;
if (pageNum) url += `&page=${pageNum}`;
const allRepos = await fetch(url).then(res => res.json()).catch(() => []);
if (!allRepos.items) {
Spicetify.showNotification("Too Many Requests, Cool Down.");
}
const filteredResults = {
...allRepos,
page_count: allRepos.items.length,
items: allRepos.items.filter(item => !BLACKLIST.includes(item.html_url)),
};
return filteredResults;
}
/**
* TODO
* @param {"theme"|"extension"} type The repo type
* @param {number} pageNum The page number
* @returns TODO
*/
async function loadPageRecursive(type, pageNum) {
const pageOfRepos = await queryRepos(type, pageNum);
appendInformationToLocalStorage(pageOfRepos, type);
// Sets the amount of items that have thus been fetched
const soFarResults = ITEMS_PER_REQUEST * (pageNum - 1) + pageOfRepos.page_count;
console.log({ pageOfRepos });
const remainingResults = pageOfRepos.total_count - soFarResults;
// If still have more results, recursively fetch next page
console.log(`Parsed ${soFarResults}/${pageOfRepos.total_count} ${type}s`);
if (remainingResults > 0) return await loadPageRecursive(type, pageNum + 1); // There are more results. currentPage + 1 is the next page to fetch.
else console.log(`No more ${type} results`);
}
(async function initializePreload() {
console.log("Preloading extensions and themes...");
window.sessionStorage.clear();
const BLACKLIST = await Blacklist();
window.sessionStorage.setItem("marketplace:blacklist", JSON.stringify(BLACKLIST));
// TODO: does this work?
// The recursion isn't super clean...
// Begin by getting the themes and extensions from github
// const [extensionReposArray, themeReposArray] = await Promise.all([
await Promise.all([
loadPageRecursive("extension", 1),
loadPageRecursive("theme", 1),
]);
// let extensionsNextPage = 1;
// let themesNextPage = 1;
// do {
// extensionReposArray = await loadPage("extension", extensionsNextPage);
// appendInformationToLocalStorage(extensionReposArray, "extension");
// } while (extensionsNextPage);
// do {
// themeReposArray = await loadPage("theme", themesNextPage);
// appendInformationToLocalStorage(themeReposArray, "theme");
// } while (themesNextPage);
})();
async function appendInformationToLocalStorage(array, type) {
// This system should make it so themes and extensions are stored concurrently
for (const repo of array.items) {
const data = (type === "theme")
? await fetchThemeManifest(repo.contents_url, repo.default_branch)
: await fetchExtensionManifest(repo.contents_url, repo.default_branch);
if (data) {
addToSessionStorage(data);
await sleep(5000);
}
}
}
// This function is used to fetch manifest of a theme and return it
async function fetchThemeManifest(contents_url, branch) {
try {
const regex_result = contents_url.match(/https:\/\/api\.github\.com\/repos\/(?<user>.+)\/(?<repo>.+)\/contents/);
// TODO: err handling?
if (!regex_result || !regex_result.groups) return null;
let { user, repo } = regex_result.groups;
let manifests = await getRepoManifest(user, repo, branch);
// If the manifest returned is not an array, initialize it as one
if (!Array.isArray(manifests)) manifests = [manifests];
manifests.user = user;
manifests.repo = repo;
if (manifests[0] && manifests[0].name && manifests[0].usercss && manifests[0].description) {
return manifests;
}
return null;
}
catch (err) {
// console.warn(contents_url, err);
return null;
}
}
// This function is used to fetch manifest of an extension and return it
async function fetchExtensionManifest(contents_url, branch) {
try {
// TODO: use the original search full_name ("theRealPadster/spicetify-hide-podcasts") or something to get the url better?
const regex_result = contents_url.match(/https:\/\/api\.github\.com\/repos\/(?<user>.+)\/(?<repo>.+)\/contents/);
// TODO: err handling?
if (!regex_result || !regex_result.groups) return null;
const { user, repo } = regex_result.groups;
let manifests = await getRepoManifest(user, repo, branch);
// If the manifest returned is not an array, initialize it as one
if (!Array.isArray(manifests)) manifests = [manifests];
manifests.user = user;
manifests.repo = repo;
if (manifests[0] && manifests[0].name && manifests[0].description && manifests[0].main) {
return manifests;
}
return null;
}
catch (err) {
// console.warn(contents_url, err);
return null;
}
}
async function getRepoManifest(user, repo, branch) {
const sessionStorageItem = window.sessionStorage.getItem(`${user}-${repo}`);
const failedSessionStorageItems = window.sessionStorage.getItem("noManifests");
if (sessionStorageItem) {
return null;
}
const url = `https://raw.githubusercontent.com/${user}/${repo}/${branch}/manifest.json`;
if (failedSessionStorageItems?.includes(url)) {
return null;
}
return await fetch(url).then(res => res.json()).catch(() => addToSessionStorage([url], "noManifests"));
}
// This function appends an array to session storage
function addToSessionStorage(items, key) {
if (!items || items == null) return;
items.forEach(item => {
if (!key) key = `${items.user}-${items.repo}`;
// If the key already exists, it will append to it instead of overwriting it
const existing = window.sessionStorage.getItem(key);
const parsed = existing ? JSON.parse(existing) : [];
parsed.push(item);
window.sessionStorage.setItem(key, JSON.stringify(parsed));
});
}
// This function is used to sleep for a certain amount of time
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

View file

@ -0,0 +1,838 @@
/// <reference types="react" />
/// <reference types="react-dom" />
/// <reference path="../spicetify-cli/globals.d.ts" />
/// <reference path="../spicetify-cli/jsHelper/spicetifyWrapper.js" />
/// <reference path="src/Card.js" />
/// <reference path="src/Icons.js" />
/// <reference path="src/Settings.js" />
/// <reference path="src/SortBox.js" />
/// <reference path="src/TabBar.js" />
/// <reference path="src/ReadmePage.js" />
/// <reference path="src/Utils.js" />
/* eslint-disable no-redeclare, no-unused-vars */
/** @type {React} */
const react = Spicetify.React;
/** @type {import("react-dom")} */
const reactDOM = Spicetify.ReactDOM;
const {
URI,
React: { useState, useEffect, useCallback },
// @ts-ignore
Platform: { History },
} = Spicetify;
/* eslint-enable no-redeclare, no-unused-vars */
// eslint-disable-next-line no-redeclare
const LOCALSTORAGE_KEYS = {
"installedExtensions": "marketplace:installed-extensions",
"installedSnippets": "marketplace:installed-snippets",
"installedThemes": "marketplace:installed-themes",
"activeTab": "marketplace:active-tab",
"tabs": "marketplace:tabs",
"sortBy": "marketplace:sort-by",
// Theme installed store the localsorage key of the theme (e.g. marketplace:installed:NYRI4/Comfy-spicetify/user.css)
"themeInstalled": "marketplace:theme-installed",
};
// Define a function called "render" to specify app entry point
// This function will be used to mount app to main view.
// eslint-disable-next-line no-unused-vars
function render() {
const { location } = Spicetify.Platform.History;
// If page state set to display readme, render it
// (This location state data comes from Card.openReadme())
if (location.pathname === "/spicetify-marketplace/readme") {
return react.createElement(ReadmePage, {
title: "Spicetify Marketplace - Readme",
data: location.state.data,
});
} // Otherwise, render the main Grid
else {
return react.createElement(Grid, { title: "Spicetify Marketplace" });
}
}
// Initalize topbar tabs
// Data initalized in TabBar.js
// eslint-disable-next-line no-redeclare
const ALL_TABS = [
{ name: "Extensions", enabled: true },
{ name: "Themes", enabled: true },
{ name: "Snippets", enabled: true },
{ name: "Installed", enabled: true },
];
let tabsString = localStorage.getItem(LOCALSTORAGE_KEYS.tabs);
let tabs = [];
try {
tabs = JSON.parse(tabsString);
if (!Array.isArray(tabs)) {
throw new Error("Could not parse marketplace tabs key");
} else if (tabs.length === 0) {
throw new Error("Empty marketplace tabs key");
} else if (tabs.filter(tab => !tab).length > 0) {
throw new Error("Falsey marketplace tabs key");
}
} catch {
tabs = ALL_TABS;
localStorage.setItem(LOCALSTORAGE_KEYS.tabs, JSON.stringify(tabs));
}
// Get active theme
let schemes = [];
let activeScheme = null;
try {
const installedThemeKey = localStorage.getItem(LOCALSTORAGE_KEYS.themeInstalled);
if (installedThemeKey) {
const installedThemeDataStr = localStorage.getItem(installedThemeKey);
if (!installedThemeDataStr) throw new Error("No installed theme data");
const installedTheme = JSON.parse(installedThemeDataStr);
schemes = installedTheme.schemes;
activeScheme = installedTheme.activeScheme;
} else {
console.log("No theme set as installed");
}
} catch (err) {
console.error(err);
}
// eslint-disable-next-line no-redeclare
const CONFIG = {
// Fetch the settings and set defaults. Used in Settings.js
visual: {
stars: JSON.parse(localStorage.getItem("marketplace:stars")) ?? true,
tags: JSON.parse(localStorage.getItem("marketplace:tags")) ?? true,
hideInstalled: JSON.parse(localStorage.getItem("marketplace:hideInstalled")) ?? false,
colorShift: JSON.parse(localStorage.getItem("marketplace:colorShift")) ?? false,
// Legacy from reddit app...
type: JSON.parse(localStorage.getItem("marketplace:type")) ?? false,
// I was considering adding watchers as "followers" but it looks like the value is a duplicate
// of stargazers, and the subscribers_count isn't returned in the main API call we make
// https://github.community/t/bug-watchers-count-is-the-duplicate-of-stargazers-count/140865/4
followers: JSON.parse(localStorage.getItem("marketplace:followers")) ?? false,
},
tabs,
activeTab: localStorage.getItem(LOCALSTORAGE_KEYS.activeTab),
theme: {
activeThemeKey: localStorage.getItem(LOCALSTORAGE_KEYS.themeInstalled) || null,
schemes,
activeScheme,
},
};
if (!CONFIG.activeTab || !CONFIG.tabs.filter(tab => tab.name === CONFIG.activeTab).length) {
CONFIG.activeTab = CONFIG.tabs[0].name;
}
// Fetches the sorting options, fetched from SortBox.js
// eslint-disable-next-line no-redeclare
let sortConfig = {
by: localStorage.getItem(LOCALSTORAGE_KEYS.sortBy) || "top",
};
let cardList = [];
let endOfList = false;
let lastScroll = 0;
let requestQueue = [];
let requestPage = null;
// Max GitHub API items per page
// https://docs.github.com/en/rest/reference/search#search-repositories
const ITEMS_PER_REQUEST = 100;
let BLACKLIST = [];
// eslint-disable-next-line no-redeclare, no-unused-vars
let gridUpdateTabs, gridUpdatePostsVisual;
class Grid extends react.Component {
constructor(props) {
super(props);
Object.assign(this, props);
this.state = {
cards: [],
tabs: CONFIG.tabs,
rest: true,
endOfList: endOfList,
schemes: CONFIG.theme.schemes,
activeScheme: CONFIG.theme.activeScheme,
activeThemeKey: CONFIG.theme.activeThemeKey,
};
}
// TODO: should I put this in Grid state?
getInstalledTheme() {
const installedThemeKey = localStorage.getItem(LOCALSTORAGE_KEYS.themeInstalled);
if (!installedThemeKey) return null;
const installedThemeDataStr = localStorage.getItem(installedThemeKey);
if (!installedThemeDataStr) return null;
const installedTheme = JSON.parse(installedThemeDataStr);
return installedTheme;
}
newRequest(amount) {
cardList = [];
const queue = [];
requestQueue.unshift(queue);
this.loadAmount(queue, amount);
}
/**
* @param {Object} item
* @param {"extension" | "theme" | "snippet"} type The type of card
*/
appendCard(item, type) {
item.visual = CONFIG.visual;
// Set key prop so items don't get stuck when switching tabs
item.key = `${CONFIG.activeTab}:${item.title}`;
item.type = type;
// Pass along the functions to update Grid state on apply
item.updateColourSchemes = this.updateColourSchemes.bind(this);
item.updateActiveTheme = this.setActiveTheme.bind(this);
// This isn't used other than to trigger a re-render
item.activeThemeKey = this.state.activeThemeKey;
cardList.push(react.createElement(Card, item));
this.setState({ cards: cardList });
}
// TODO: this isn't currently used, but it will be used for sorting (based on the SortBox component)
updateSort(sortByValue) {
if (sortByValue) {
sortConfig.by = sortByValue;
localStorage.setItem(LOCALSTORAGE_KEYS.sortBy, sortByValue);
}
requestPage = null;
cardList = [];
this.setState({
cards: [],
rest: false,
endOfList: false,
});
endOfList = false;
this.newRequest(ITEMS_PER_REQUEST);
}
updateTabs() {
this.setState({
tabs: [...CONFIG.tabs],
});
}
updatePostsVisual() {
cardList = cardList.map(card => {
return react.createElement(Card, card.props);
});
this.setState({ cards: [...cardList] });
}
switchTo(value) {
CONFIG.activeTab = value;
localStorage.setItem(LOCALSTORAGE_KEYS.activeTab, value);
cardList = [];
requestPage = null;
this.setState({
cards: [],
rest: false,
endOfList: false,
});
endOfList = false;
this.newRequest(ITEMS_PER_REQUEST);
}
// This is called from loadAmount in a loop until it has the requested amount of cards or runs out of results
// Returns the next page number to fetch, or null if at end
// TODO: maybe we should rename `loadPage()`, since it's slightly confusing when we have github pages as well
async loadPage(queue) {
if (CONFIG.activeTab === "Extensions") {
let pageOfRepos = await getExtensionRepos(requestPage);
for (const repo of pageOfRepos.items) {
let extensions = await fetchExtensionManifest(repo.contents_url, repo.default_branch, repo.stargazers_count);
// I believe this stops the requests when switching tabs?
if (requestQueue.length > 1 && queue !== requestQueue[0]) {
// Stop this queue from continuing to fetch and append to cards list
return -1;
}
if (extensions && extensions.length) {
// console.log(`${repo.name} has ${extensions.length} extensions:`, extensions);
extensions.forEach((extension) => this.appendCard(extension, "extension"));
}
}
// First result is null or -1 so it coerces to 1
const currentPage = requestPage > -1 && requestPage ? requestPage : 1;
// Sets the amount of items that have thus been fetched
const soFarResults = ITEMS_PER_REQUEST * (currentPage - 1) + pageOfRepos.page_count;
const remainingResults = pageOfRepos.total_count - soFarResults;
// If still have more results, return next page number to fetch
console.log(`Parsed ${soFarResults}/${pageOfRepos.total_count} extensions`);
if (remainingResults > 0) return currentPage + 1;
else console.log("No more extension results");
} else if (CONFIG.activeTab === "Installed") {
const installedStuff = {
theme: getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedThemes, []),
extension: getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedExtensions, []),
snippet: getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedSnippets, []),
};
for (const type in installedStuff) {
if (installedStuff[type].length) {
installedStuff[type].forEach(async (itemKey) => {
// TODO: err handling
const extension = JSON.parse(localStorage.getItem(itemKey));
// I believe this stops the requests when switching tabs?
if (requestQueue.length > 1 && queue !== requestQueue[0]) {
// Stop this queue from continuing to fetch and append to cards list
return -1;
}
// @ts-ignore
this.appendCard(extension, type);
});
}
}
// Don't need to return a page number because
// installed extension do them all in one go, since it's local
} else if (CONFIG.activeTab == "Themes") {
let pageOfRepos = await getThemeRepos(requestPage);
for (const repo of pageOfRepos.items) {
let themes = await fetchThemeManifest(repo.contents_url, repo.default_branch, repo.stargazers_count);
// I believe this stops the requests when switching tabs?
if (requestQueue.length > 1 && queue !== requestQueue[0]) {
// Stop this queue from continuing to fetch and append to cards list
return -1;
}
if (themes && themes.length) {
themes.forEach((theme) => this.appendCard(theme, "theme"));
}
}
// First request is null, so coerces to 1
const currentPage = requestPage > -1 && requestPage ? requestPage : 1;
// -1 because the page number is 1-indexed
const soFarResults = ITEMS_PER_REQUEST * (currentPage - 1) + pageOfRepos.page_count;
const remainingResults = pageOfRepos.total_count - soFarResults;
console.log(`Parsed ${soFarResults}/${pageOfRepos.total_count} themes`);
if (remainingResults > 0) return currentPage + 1;
else console.log("No more theme results");
} else if (CONFIG.activeTab == "Snippets") {
let snippets = await fetchCssSnippets();
if (requestQueue.length > 1 && queue !== requestQueue[0]) {
// Stop this queue from continuing to fetch and append to cards list
return -1;
}
if (snippets && snippets.length) {
snippets.forEach((snippet) => this.appendCard(snippet, "snippet"));
}
}
this.setState({ rest: true, endOfList: true });
endOfList = true;
return null;
}
/**
* Load a new set of extensions
* @param {any} queue An array of the extensions to be loaded
* @param {number} [quantity] Amount of extensions to be loaded per page. (Defaults to ITEMS_PER_REQUEST constant)
*/
async loadAmount(queue, quantity = ITEMS_PER_REQUEST) {
this.setState({ rest: false });
quantity += cardList.length;
requestPage = await this.loadPage(queue);
while (
requestPage &&
requestPage !== -1 &&
cardList.length < quantity &&
!this.state.endOfList
) {
requestPage = await this.loadPage(queue);
}
if (requestPage === -1) {
requestQueue = requestQueue.filter(a => a !== queue);
return;
}
// Remove this queue from queue list
requestQueue.shift();
this.setState({ rest: true });
}
/**
* Load more items if there are more items to load.
* @returns {void}
*/
loadMore() {
if (this.state.rest && !endOfList) {
this.loadAmount(requestQueue[0], ITEMS_PER_REQUEST);
}
}
/**
* Update the colour schemes in the state + dropdown, and inject the active one
* @param {any} schemes Object with the colour schemes
* @param {string} activeScheme The name of the active colour scheme (a key in the schemes object)
*/
updateColourSchemes(schemes, activeScheme) {
console.log("updateColourSchemes", schemes, activeScheme);
CONFIG.theme.schemes = schemes;
CONFIG.theme.activeScheme = activeScheme;
if (schemes && schemes[activeScheme]) {
this.injectColourScheme(CONFIG.theme.schemes[activeScheme]);
} else {
// Reset schemes if none sent
this.injectColourScheme(null);
}
// Save to localstorage
const installedThemeKey = localStorage.getItem(LOCALSTORAGE_KEYS.themeInstalled);
const installedThemeDataStr = localStorage.getItem(installedThemeKey);
const installedThemeData = JSON.parse(installedThemeDataStr);
installedThemeData.activeScheme = activeScheme;
localStorage.setItem(installedThemeKey, JSON.stringify(installedThemeData));
this.setState({
schemes,
activeScheme,
});
}
// NOTE: Keep in sync with extension.js
injectColourScheme (scheme) {
// Remove any existing Spicetify scheme
const existingColorsCSS = document.querySelector("link[href='colors.css']");
if (existingColorsCSS) existingColorsCSS.remove();
// Remove any existing marketplace scheme
const existingMarketplaceSchemeCSS = document.querySelector("style.marketplaceCSS.marketplaceScheme");
if (existingMarketplaceSchemeCSS) existingMarketplaceSchemeCSS.remove();
if (scheme) {
// Add new marketplace scheme
const schemeTag = document.createElement("style");
schemeTag.classList.add("marketplaceCSS");
schemeTag.classList.add("marketplaceScheme");
let injectStr = ":root {";
const themeIniKeys = Object.keys(scheme);
themeIniKeys.forEach((key) => {
injectStr += `--spice-${key}: #${scheme[key]};`;
injectStr += `--spice-rgb-${key}: ${hexToRGB(scheme[key])};`;
});
injectStr += "}";
schemeTag.innerHTML = injectStr;
document.head.appendChild(schemeTag);
} else {
// Re-add default user.css
let originalColorsCSS = document.createElement("link");
originalColorsCSS.setAttribute("rel", "stylesheet");
originalColorsCSS.setAttribute("href", "colors.css");
originalColorsCSS.classList.add("userCSS");
document.head.appendChild(originalColorsCSS);
}
}
/**
* The componentDidMount() method is called when the component is first loaded.
* It checks if the cardList is already loaded. If it is, it checks if the lastScroll value is
greater than 0.
* If it is, it scrolls to the lastScroll value. If it isn't, it scrolls to the top of the page.
* If the cardList isn't loaded, it loads the cardList.
*/
async componentDidMount() {
gridUpdateTabs = this.updateTabs.bind(this);
gridUpdatePostsVisual = this.updatePostsVisual.bind(this);
const viewPort = document.querySelector(".os-viewport");
this.checkScroll = this.isScrolledBottom.bind(this);
viewPort.addEventListener("scroll", this.checkScroll);
if (cardList.length) { // Already loaded
if (lastScroll > 0) {
viewPort.scrollTo(0, lastScroll);
}
return;
}
// Load blacklist
BLACKLIST = await getBlacklist();
this.newRequest(ITEMS_PER_REQUEST);
}
/**
* When the component is unmounted, remove the scroll event listener.
* @returns {void}
*/
componentWillUnmount() {
gridUpdateTabs = gridUpdatePostsVisual = null;
const viewPort = document.querySelector(".os-viewport");
lastScroll = viewPort.scrollTop;
viewPort.removeEventListener("scroll", this.checkScroll);
}
/**
* If the user has scrolled to the bottom of the page, load more posts.
* @param event - The event object that is passed to the callback function.
* @returns {void}
*/
isScrolledBottom(event) {
const viewPort = event.target;
if ((viewPort.scrollTop + viewPort.clientHeight) >= viewPort.scrollHeight) {
// At bottom, load more posts
this.loadMore();
}
}
setActiveTheme(themeKey) {
CONFIG.theme.activeThemeKey = themeKey;
this.setState({ activeThemeKey: themeKey });
}
// TODO: clean this up. It worked when I was using state, but state seems like pointless overhead.
getActiveScheme() {
return this.state.activeScheme;
}
render() {
return react.createElement("section", {
className: "contentSpacing",
},
react.createElement("div", {
className: "marketplace-header",
}, react.createElement("h1", null, this.props.title),
// Start of marketplace-header__right
react.createElement("div", {
className: "marketplace-header__right",
},
// Show colour scheme dropdown if there is a theme with schemes installed
this.state.activeScheme ? react.createElement(SortBox, {
onChange: (value) => this.updateColourSchemes(this.state.schemes, value),
// TODO: Make this compatible with the changes to the theme install process: need to create a method to update the scheme options without a full reload.
sortBoxOptions: generateSchemesOptions(this.state.schemes),
// It doesn't work when I directly use CONFIG.theme.activeScheme in the sortBySelectedFn
// because it hardcodes the value into the fn
sortBySelectedFn: (a) => a.key === this.getActiveScheme(),
}) : null,
react.createElement("button", {
className: "marketplace-settings-button",
id: "marketplace-settings-button",
onClick: openConfig,
}, SETTINGS_ICON),
// End of marketplace-header__right
),
// TODO: Add search bar and sort functionality
// react.createElement("div", {
// className: "searchbar--bar__wrapper",
// }, react.createElement("input", {
// className: "searchbar-bar",
// type: "text",
// placeholder: "Search for Extensions?",
// })),
),
[ // Add a header and grid for each card type if it has any cards
{ handle: "extension", name: "Extensions" },
{ handle: "theme", name: "Themes" },
{ handle: "snippet", name: "Snippets" },
].map((cardType) => {
const cardsOfType = cardList.filter((card) => card.props.type === cardType.handle)
.map((card) => {
// Clone the cards and update the prop to trigger re-render
// TODO: is it possible to only re-render the theme cards whose status have changed?
const cardElement = react.cloneElement(card, {
activeThemeKey: this.state.activeThemeKey,
});
return cardElement;
});
if (cardsOfType.length) {
return [
// Add a header for the card type
react.createElement("h2",
{ className: "marketplace-card-type-heading" },
cardType.name),
// Add the grid and cards
react.createElement("div", {
className: "marketplace-grid main-gridContainer-gridContainer main-gridContainer-fixedWidth",
"data-tab": CONFIG.activeTab,
style: {
"--minimumColumnWidth": "180px",
"--column-width": "minmax(var(--minimumColumnWidth),1fr)",
"--column-count": "auto-fill",
"--grid-gap": "24px",
},
}, cardsOfType)];
} else {
return null;
}
}), react.createElement("footer", {
style: {
margin: "auto",
textAlign: "center",
},
}, !this.state.endOfList && (this.state.rest ? react.createElement(LoadMoreIcon, { onClick: this.loadMore.bind(this) }) : react.createElement(LoadingIcon)),
// Add snippets button if on snippets tab
CONFIG.activeTab === "Snippets" ? react.createElement("button", {
className: "marketplace-add-snippet-btn main-buttons-button main-button-secondary",
onClick: openAddSnippetModal,
}, "+Add CSS") : null,
), react.createElement(TopBarContent, {
switchCallback: this.switchTo.bind(this),
links: CONFIG.tabs,
activeLink: CONFIG.activeTab,
}));
}
}
// TODO: add sort type, order, etc?
// https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-for-repositories#search-by-topic
// https://docs.github.com/en/rest/reference/search#search-repositories
/**
* Query GitHub for all repos with the "spicetify-extensions" topic
* @param {number} page The query page number
* @returns Array of search results (filtered through the blacklist)
*/
async function getExtensionRepos(page = 1) {
// www is needed or it will block with "cross-origin" error.
let url = `https://api.github.com/search/repositories?q=${encodeURIComponent("topic:spicetify-extensions")}&per_page=${ITEMS_PER_REQUEST}`;
// We can test multiple pages with this URL (58 results), as well as broken iamges etc.
// let url = `https://api.github.com/search/repositories?q=${encodeURIComponent("topic:spicetify")}`;
if (page) url += `&page=${page}`;
// Sorting params (not implemented for Marketplace yet)
// if (sortConfig.by.match(/top|controversial/) && sortConfig.time) {
// url += `&t=${sortConfig.time}`
const allRepos = await fetch(url).then(res => res.json()).catch(() => []);
if (!allRepos.items) {
Spicetify.showNotification("Too Many Requests, Cool Down.");
}
const filteredResults = {
...allRepos,
// Include count of all items on the page, since we're filtering the blacklist below,
// which can mess up the paging logic
page_count: allRepos.items.length,
items: allRepos.items.filter(item => !BLACKLIST.includes(item.html_url)),
};
return filteredResults;
}
// TODO: add try/catch here?
// TODO: can we add a return type here?
/**
* Get the manifest object for a repo
* @param {string} user Owner username
* @param {string} repo Repo name
* @param {string} branch Default branch name (e.g. main or master)
* @returns The manifest object
*/
async function getRepoManifest(user, repo, branch) {
const sessionStorageItem = window.sessionStorage.getItem(`${user}-${repo}`);
const failedSessionStorageItems = window.sessionStorage.getItem("noManifests");
if (sessionStorageItem) {
return JSON.parse(sessionStorageItem);
}
const url = `https://raw.githubusercontent.com/${user}/${repo}/${branch}/manifest.json`;
if (failedSessionStorageItems.includes(url)) {
return null;
}
const manifest = await fetch(url).then(res => res.json()).catch(() => addToSessionStorage([url], "noManifests"));
if (manifest) {
window.sessionStorage.setItem(`${user}-${repo}`, JSON.stringify(manifest));
}
return manifest;
}
// TODO: can we add a return type here?
/**
* Fetch extensions from a repo and format data for generating cards
* @param {string} contents_url The repo's GitHub API contents_url (e.g. "https://api.github.com/repos/theRealPadster/spicetify-hide-podcasts/contents/{+path}")
* @param {string} branch The repo's default branch (e.g. main or master)
* @param {number} stars The number of stars the repo has
* @returns Extension info for card (or null)
*/
async function fetchExtensionManifest(contents_url, branch, stars) {
try {
// TODO: use the original search full_name ("theRealPadster/spicetify-hide-podcasts") or something to get the url better?
let manifests;
const regex_result = contents_url.match(/https:\/\/api\.github\.com\/repos\/(?<user>.+)\/(?<repo>.+)\/contents/);
// TODO: err handling?
if (!regex_result || !regex_result.groups) return null;
const { user, repo } = regex_result.groups;
manifests = await getRepoManifest(user, repo, branch);
// If the manifest returned is not an array, initialize it as one
if (!Array.isArray(manifests)) manifests = [manifests];
// Manifest is initially parsed
const parsedManifests = manifests.reduce((accum, manifest) => {
const selectedBranch = manifest.branch || branch;
const item = {
manifest,
title: manifest.name,
subtitle: manifest.description,
authors: processAuthors(manifest.authors, user),
user,
repo,
branch: selectedBranch,
imageURL: manifest.preview && manifest.preview.startsWith("http")
? manifest.preview
: `https://raw.githubusercontent.com/${user}/${repo}/${selectedBranch}/${manifest.preview}`,
extensionURL: manifest.main.startsWith("http")
? manifest.main
: `https://raw.githubusercontent.com/${user}/${repo}/${selectedBranch}/${manifest.main}`,
readmeURL: manifest.readme && manifest.readme.startsWith("http")
? manifest.readme
: `https://raw.githubusercontent.com/${user}/${repo}/${selectedBranch}/${manifest.readme}`,
stars,
tags: manifest.tags,
};
// If manifest is valid, add it to the list
if (manifest && manifest.name && manifest.description && manifest.main
) {
// Add to list unless we're hiding installed items and it's installed
if (!(CONFIG.visual.hideInstalled
&& localStorage.getItem("marketplace:installed:" + `${user}/${repo}/${manifest.main}`))
) {
accum.push(item);
}
}
// else {
// console.error("Invalid manifest:", manifest);
// }
return accum;
}, []);
return parsedManifests;
}
catch (err) {
// console.warn(contents_url, err);
return null;
}
}
// TODO: can we add a return type here?
/**
* Fetch themes from a repo and format data for generating cards
* @param {string} contents_url The repo's GitHub API contents_url (e.g. "https://api.github.com/repos/theRealPadster/spicetify-hide-podcasts/contents/{+path}")
* @param {string} branch The repo's default branch (e.g. main or master)
* @param {number} stars The number of stars the repo has
* @returns Extension info for card (or null)
*/
async function fetchThemeManifest(contents_url, branch, stars) {
try {
let manifests;
const regex_result = contents_url.match(/https:\/\/api\.github\.com\/repos\/(?<user>.+)\/(?<repo>.+)\/contents/);
// TODO: err handling?
if (!regex_result || !regex_result.groups) return null;
let { user, repo } = regex_result.groups;
manifests = await getRepoManifest(user, repo, branch);
// If the manifest returned is not an array, initialize it as one
if (!Array.isArray(manifests)) manifests = [manifests];
// Manifest is initially parsed
const parsedManifests = manifests.reduce((accum, manifest) => {
const selectedBranch = manifest.branch || branch;
const item = {
manifest,
title: manifest.name,
subtitle: manifest.description,
authors: processAuthors(manifest.authors, user),
user,
repo,
branch: selectedBranch,
imageURL: manifest.preview && manifest.preview.startsWith("http")
? manifest.preview
: `https://raw.githubusercontent.com/${user}/${repo}/${selectedBranch}/${manifest.preview}`,
readmeURL: manifest.readme && manifest.readme.startsWith("http")
? manifest.readme
: `https://raw.githubusercontent.com/${user}/${repo}/${selectedBranch}/${manifest.readme}`,
stars,
tags: manifest.tags,
// theme stuff
cssURL: manifest.usercss.startsWith("http")
? manifest.usercss
: `https://raw.githubusercontent.com/${user}/${repo}/${selectedBranch}/${manifest.usercss}`,
// TODO: clean up indentation etc
schemesURL: manifest.schemes
? (
manifest.schemes.startsWith("http") ? manifest.schemes : `https://raw.githubusercontent.com/${user}/${repo}/${selectedBranch}/${manifest.schemes}`
)
: null,
include: manifest.include,
};
// If manifest is valid, add it to the list
if (manifest && manifest.name && manifest.usercss && manifest.description) {
accum.push(item);
}
return accum;
}, []);
return parsedManifests;
}
catch (err) {
// console.warn(contents_url, err);
return null;
}
}
/**
* Query the GitHub API for a page of theme repos (having "spicetify-themes" topic)
* @param {number} page The page to get (defaults to 1)
* @returns Array of GitHub API results, filtered through the blacklist
*/
async function getThemeRepos(page = 1) {
let url = `https://api.github.com/search/repositories?q=${encodeURIComponent("topic:spicetify-themes")}&per_page=${ITEMS_PER_REQUEST}`;
// We can test multiple pages with this URL (58 results), as well as broken iamges etc.
// let url = `https://api.github.com/search/repositories?q=${encodeURIComponent("topic:spicetify")}`;
if (page) url += `&page=${page}`;
// Sorting params (not implemented for Marketplace yet)
// if (sortConfig.by.match(/top|controversial/) && sortConfig.time) {
// url += `&t=${sortConfig.time}`
const allThemes = await fetch(url).then(res => res.json()).catch(() => []);
if (!allThemes.items) {
Spicetify.showNotification("Too Many Requests, Cool Down.");
}
const filteredResults = {
...allThemes,
// Include count of all items on the page, since we're filtering the blacklist below,
// which can mess up the paging logic
page_count: allThemes.items.length,
items: allThemes.items.filter(item => !BLACKLIST.includes(item.html_url)),
};
return filteredResults;
}
function addToSessionStorage(items, key) {
if (!items || items == null) return;
items.forEach(item => {
if (!key) key = `${items.user}-${items.repo}`;
// If the key already exists, it will append to it instead of overwriting it
const existing = window.sessionStorage.getItem(key);
const parsed = existing ? JSON.parse(existing) : [];
parsed.push(item);
window.sessionStorage.setItem(key, JSON.stringify(parsed));
});
}

View file

@ -0,0 +1,41 @@
# Modified from https://github.com/JulienMaille/dribbblish-dynamic-theme/blob/main/install.ps1
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "Setting up..." -ForegroundColor "Green"
$checkSpice = Get-Command spicetify -ErrorAction Silent
if ($null -eq $checkSpice) {
Write-Host -ForegroundColor Red "Spicetify not found. Installing that for you..."
Invoke-WebRequest -UseBasicParsing "https://raw.githubusercontent.com/khanhas/spicetify-cli/master/install.ps1" | Invoke-Expression
}
$sp_dir = "${HOME}\spicetify-cli\CustomApps"
if (-not (Test-Path $sp_dir)) {
Write-Host "Making a CustomApps folder..." -ForegroundColor "Cyan"
New-Item -Path $sp_dir -ItemType Directory | Out-Null
Write-Done
}
$spicePath = spicetify -c | Split-Path
$sp_dot_dir = "$spicePath\CustomApps"
if (-not (Test-Path $sp_dot_dir)) {
Write-Host "Making a CustomApps folder..." -ForegroundColor "Cyan"
New-Item -Path $sp_dot_dir -ItemType Directory | Out-Null
}
Write-Host "Downloading..." -ForegroundColor "Green"
Invoke-WebRequest -Uri "https://github.com/CharlieS1103/spicetify-marketplace/archive/refs/heads/main.zip" -UseBasicParsing -OutFile "${HOME}/spicetify-cli/CustomApps/spicetify-marketplace.zip"
Write-Host "Unzipping and installing..." -ForegroundColor "Green"
Expand-Archive -Path "${HOME}/spicetify-cli/CustomApps/spicetify-marketplace.zip" -DestinationPath "${HOME}/spicetify-cli/CustomApps/" -Force
Remove-Item -Path "${HOME}/spicetify-cli/CustomApps/spicetify-marketplace.zip" -Force
if (Test-Path -Path "${HOME}/spicetify-cli/CustomApps/spicetify-marketplace") {
Write-Host "spicetify-marketplace was already found! Updating..." -ForegroundColor "Cyan"
Remove-Item -Path "${HOME}/spicetify-cli/CustomApps/spicetify-marketplace" -Force -Recurse
}
Rename-Item -Path "${HOME}/spicetify-cli/CustomApps/spicetify-marketplace-main" -NewName "spicetify-marketplace" -Force
Copy-Item -Path "${HOME}/spicetify-cli/CustomApps/spicetify-marketplace" -Destination $sp_dot_dir -Recurse -Force
spicetify config custom_apps spicetify-marketplace
spicetify backup apply
Write-Host "Done! If nothing has happened, do spicetify apply" -ForegroundColor "Green"

View file

@ -0,0 +1,20 @@
{
"name": "Marketplace",
"icon": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 576 512\"><path fill=\"currentColor\" d=\"M504.717 320H211.572l6.545 32h268.418c15.401 0 26.816 14.301 23.403 29.319l-5.517 24.276C523.112 414.668 536 433.828 536 456c0 31.202-25.519 56.444-56.824 55.994-29.823-.429-54.35-24.631-55.155-54.447-.44-16.287 6.085-31.049 16.803-41.548H231.176C241.553 426.165 248 440.326 248 456c0 31.813-26.528 57.431-58.67 55.938-28.54-1.325-51.751-24.385-53.251-52.917-1.158-22.034 10.436-41.455 28.051-51.586L93.883 64H24C10.745 64 0 53.255 0 40V24C0 10.745 10.745 0 24 0h102.529c11.401 0 21.228 8.021 23.513 19.19L159.208 64H551.99c15.401 0 26.816 14.301 23.403 29.319l-47.273 208C525.637 312.246 515.923 320 504.717 320zM403.029 192H360v-60c0-6.627-5.373-12-12-12h-24c-6.627 0-12 5.373-12 12v60h-43.029c-10.691 0-16.045 12.926-8.485 20.485l67.029 67.029c4.686 4.686 12.284 4.686 16.971 0l67.029-67.029c7.559-7.559 2.205-20.485-8.486-20.485z\"></path></svg>",
"active-icon": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 576 512\"><path fill=\"currentColor\" d=\"M504.717 320H211.572l6.545 32h268.418c15.401 0 26.816 14.301 23.403 29.319l-5.517 24.276C523.112 414.668 536 433.828 536 456c0 31.202-25.519 56.444-56.824 55.994-29.823-.429-54.35-24.631-55.155-54.447-.44-16.287 6.085-31.049 16.803-41.548H231.176C241.553 426.165 248 440.326 248 456c0 31.813-26.528 57.431-58.67 55.938-28.54-1.325-51.751-24.385-53.251-52.917-1.158-22.034 10.436-41.455 28.051-51.586L93.883 64H24C10.745 64 0 53.255 0 40V24C0 10.745 10.745 0 24 0h102.529c11.401 0 21.228 8.021 23.513 19.19L159.208 64H551.99c15.401 0 26.816 14.301 23.403 29.319l-47.273 208C525.637 312.246 515.923 320 504.717 320zM403.029 192H360v-60c0-6.627-5.373-12-12-12h-24c-6.627 0-12 5.373-12 12v60h-43.029c-10.691 0-16.045 12.926-8.485 20.485l67.029 67.029c4.686 4.686 12.284 4.686 16.971 0l67.029-67.029c7.559-7.559 2.205-20.485-8.486-20.485z\"></path></svg>",
"subfiles": [
"src/Card.js",
"src/Icons.js",
"src/OptionsMenu.js",
"src/SortBox.js",
"src/TabBar.js",
"src/Settings.js",
"src/ReadmePage.js",
"src/ReloadModal.js",
"src/AddSnippetModal.js",
"src/Utils.js"
],
"subfiles_extension": [
"extension.js"
]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
{
"name": "spicetify-marketplace",
"homepage": "https://github.com/CharlieS1103/spicetify-marketplace",
"repository": {
"type": "git",
"url": "https://github.com/CharlieS1103/spicetify-marketplace.git"
},
"bugs": {
"url": "https://github.com/CharlieS1103/spicetify-marketplace/issues"
},
"scripts": {
"type-check": "tsc",
"watch": "parcel watch",
"build": "parcel build",
"lint": "eslint src"
},
"source": "styles/style.scss",
"targets": {
"default": {
"distDir": ".",
"optimize": false
}
},
"engines": {
"node": ">=16"
},
"devDependencies": {
"@parcel/transformer-sass": "^2.3.2",
"@types/react": "^17.0.40",
"@types/react-dom": "^17.0.11",
"eslint": "^8.11.0",
"eslint-plugin-react": "^7.29.3",
"parcel": "^2.3.2"
}
}

View file

@ -0,0 +1,57 @@
[
{
"title": "Fix 'Episodes' Icon",
"description": "Fix the color of the Episodes icon",
"code": ".main-yourEpisodesButton-yourEpisodesIcon { background: var(--spice-text); } .main-yourEpisodesButton-yourEpisodesIcon path { fill: var(--spice-player); opacity: 0.7; }"
},
{
"title": "Fix 'Liked' Icon",
"description": "Fix the colours of the Liked icon in sidebar",
"code": ".main-likedSongsButton-likedSongsIcon { color: var(--spice-sidebar); background: var(--spice-text); }"
},
{
"title": "Fix 'Made For You' Icon",
"description": "Fix the colours of the Made For You icon",
"code": ".utp540VMXpC3bYIxo9wy { color: var(--spice-sidebar); background: var(--spice-text); opacity: 1; } .t6d9Hf7maqFPHp1Y02VR { color: var(--spice-text); }"
},
{
"title": "Hide Upgrade Button",
"description": "Makes the upgrade button hidden",
"code": ".main-topBar-UpgradeButton { display: none; }"
},
{
"title": "Auto-hide Friends",
"description": "Collapse the friends activity sidebar on small screens",
"code": ".main-buddyFeed-buddyFeedRoot { transition: width 0.3s; } @media screen and (max-width: 1200px) { .main-buddyFeed-buddyFeedRoot { width: 0; } }"
},
{
"title": "Smooth Reveal Playlist Gradient",
"description": "Reveals the playlist gradient header gradient with a fade in effect",
"code": ".main-entityHeader-overlay, .main-actionBarBackground-background, .main-entityHeader-overlay, .main-entityHeader-backgroundColor { -webkit-transition: 3s; }"
},
{
"title": "Fix progress bar displacement",
"description": "Fix the progress bar displacement when listening on different devices",
"code": ".main-connectBar-connectBar { overflow: visible !important; --triangle-position: 147px !important; align-items: unset !important; height: 0px !important; position: absolute !important; left: 80% !important; display: flex !important; bottom: 2% !important; padding: unset !important; }"
},
{
"title": "Fix playlist arrows",
"description": "Fixes the opened and closed orientation of the playlist folder arrows",
"code": ".main-rootlist-expandArrow { -webkit-transform: rotate(-90deg) !important; transform: rotate(-90deg) !important; } .main-rootlist-expandArrow:hover { -webkit-transform: rotate(-90deg) !important; transform: rotate(-90deg) !important; } .qAAhQw9dXNB7DbPgDDxy { -webkit-transform: rotate(0deg) !important; transform: rotate(0deg) !important; } .qAAhQw9dXNB7DbPgDDxy:hover { -webkit-transform: rotate(0deg) !important; transform: rotate(0deg) !important; }"
},
{
"title": "Fix main view width",
"description": "Makes main view fill up all available space",
"code": ".contentSpacing { max-width: 100% !important; }"
},
{
"title": "Left aligned heart icons",
"description": "Moves the heart icon to the left side of the track title in track views",
"code": ".main-trackList-rowSectionStart { margin-left: 38px !important; } .main-addButton-button.main-trackList-rowHeartButton, .main-addButton-button.main-trackList-rowHeartButton { position: absolute !important; left: 48px !important; }"
},
{
"title": "Disable Hover Panels [comfy-shadow]",
"description": "For comfy shadow this disables the hovering of panels and bars.",
"code": "nav.Root__nav-bar { position: inherit !important; width: calc(var(--nav-bar-width) + 9px) !important; opacity: 1 !important; transition: none !important; } nav.Root__nav-bar:hover { position: inherit !important; width: calc(var(--nav-bar-width) + 9px) !important; opacity: 1 !important; transition: none !important; } .LayoutResizer__resize-bar { cursor: col-resize !important; } .Root__top-bar{ opacity: 1 !important; transition: none !important; } .Root__top-bar:hover { transition-delay: none !important; opacity: 1 !important; transition: none !important; } .main-topBar-container { padding-left: 32px !important; } aside.main-buddyFeed-buddyFeedRoot:hover { position: relative !important; width: var(--buddy-feed-width) !important; opacity: 1 !important; bottom: 0 !important; left: 0 !important; top: 0 !important; z-index: 1 !important; } aside.main-buddyFeed-buddyFeedRoot { position: relative !important; width: var(--buddy-feed-width) !important; opacity: 1 !important; bottom: 0 !important; left: 0 !important; top: 0 !important; z-index: 1 !important; }"
}
]

View file

@ -0,0 +1,116 @@
let addSnippetContainer;
// eslint-disable-next-line no-unused-vars, no-redeclare
function openAddSnippetModal() {
const MODAL_TITLE = "Add Snippet";
const triggerModal = () => {
Spicetify.PopupModal.display({
title: MODAL_TITLE,
content: addSnippetContainer,
isLarge: true,
});
};
if (addSnippetContainer) {
triggerModal();
return;
}
addSnippetContainer = document.createElement("div");
addSnippetContainer.id = "marketplace-add-snippet-container";
// Code section =====
const codeContainer = document.createElement("div");
codeContainer.className = "marketplace-customCSS-input-container";
const codeLabel = document.createElement("label");
codeLabel.setAttribute("for", "marketplace-custom-css");
codeLabel.innerText = "Custom CSS";
codeContainer.appendChild(codeLabel);
const textArea = document.createElement("textarea");
textArea.id = "marketplace-custom-css";
textArea.name = "marketplace-custom-css";
textArea.rows = "4";
textArea.cols = "50";
textArea.placeholder = "Input your own custom CSS here! You can find them in the installed tab for management.";
codeContainer.appendChild(textArea);
// Name section =====
const nameContainer = document.createElement("div");
nameContainer.className = "marketplace-customCSS-input-container";
const nameLabel = document.createElement("label");
nameLabel.setAttribute("for", "marketplace-customCSS-name-submit");
nameLabel.innerText = "Snippet Name";
nameContainer.appendChild(nameLabel);
const nameInput = document.createElement("input");
nameInput.id = "marketplace-customCSS-name-submit";
nameInput.name = "marketplace-customCSS-name-submit";
nameInput.placeholder = "Enter a name for your custom snippet.";
nameContainer.appendChild(nameInput);
// Description section =====
const descriptionContainer = document.createElement("div");
descriptionContainer.className = "marketplace-customCSS-input-container";
const descriptionLabel = document.createElement("label");
descriptionLabel.setAttribute("for", "marketplace-customCSS-description-submit");
descriptionLabel.innerText = "Snippet Description";
descriptionContainer.appendChild(descriptionLabel);
const descriptionInput = document.createElement("input");
descriptionInput.id = "marketplace-customCSS-description-submit";
descriptionInput.name = "marketplace-customCSS-description-submit";
descriptionInput.placeholder = "Enter a description for your custom snippet.";
descriptionContainer.appendChild(descriptionInput);
// Submit button =====
const submitBtn = document.createElement("button");
submitBtn.className = "main-buttons-button main-button-secondary";
submitBtn.id = "marketplace-customCSS-submit";
submitBtn.innerText = "Save CSS";
submitBtn.addEventListener("click", function(event) {
event.preventDefault();
// @ts-ignore
const code = textArea.value.replace(/\n/g, "");
// @ts-ignore
const name = nameInput.value.replace(/\n/g, "");
const description = descriptionInput.value.trim();
const localStorageKey = `marketplace:installed:snippet:${name}`;
if (getLocalStorageDataFromKey(localStorageKey)) {
alert("That name is already taken!");
return;
}
console.log(`Installing snippet: ${name}`);
localStorage.setItem(localStorageKey, JSON.stringify({
code,
description,
title: name,
}));
// Add to installed list if not there already
const installedSnippetKeys = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedSnippets, []);
if (installedSnippetKeys.indexOf(localStorageKey) === -1) {
installedSnippetKeys.push(localStorageKey);
localStorage.setItem(LOCALSTORAGE_KEYS.installedSnippets, JSON.stringify(installedSnippetKeys));
}
const installedSnippets = installedSnippetKeys.map((key) => getLocalStorageDataFromKey(key));
initializeSnippets(installedSnippets);
Spicetify.PopupModal.hide();
}, false);
addSnippetContainer.append(
codeContainer,
nameContainer,
descriptionContainer,
submitBtn,
);
triggerModal();
}

View file

@ -0,0 +1,592 @@
/// <reference path="ReloadModal.js" />
// eslint-disable-next-line no-redeclare, no-unused-vars
class Card extends react.Component {
constructor(props) {
super(props);
this.MAX_TAGS = 4;
// From `appendCard()`
/** @type { { type: string; stars: string; } } */
this.visual;
/** @type { "extension" | "theme" | "snippet" } */
this.type;
/** @type { (any, string) => void } */
this.updateColourSchemes = props.updateColourSchemes;
/** @type { (string) => void } */
this.updateActiveTheme = props.updateActiveTheme;
// From `fetchExtensionManifest()`, `fetchThemeManifest()`, and snippets.json
/** @type { {
* name: string;
* description: string;
* main: string;
* authors: { name: string; url: string; }[];
* preview: string;
* readme: string;
* tags?: string[];
* code?: string;
* usercss?: string;
* schemes?: string;
* include?: string[]
* } } */
this.manifest;
/** @type { string } */
this.title;
/** @type { string } */
this.subtitle;
/** @type { { name: string; url: string; }[] } */
this.authors;
/** @type { string } */
this.repo;
/** @type { string } */
this.user;
/** @type { string } */
this.branch;
/** @type { string } */
this.imageURL;
/** @type { string } */
this.extensionURL;
/** @type { string } */
this.readmeURL;
/** @type { number } */
this.stars;
// Theme stuff
/** @type { string? } */
this.cssURL;
/** @type { string? } */
this.schemesURL;
/** @type { string[]? } */
this.include;
// Snippet stuff
/** @type { string? } */
this.code;
/** @type { string? } */
this.description;
/** @type { string[] } */
this.tags;
// Added locally
// this.menuType = Spicetify.ReactComponent.Menu | "div";
this.menuType = Spicetify.ReactComponent.Menu;
let prefix = props.type === "snippet" ? "snippet:" : `${props.user}/${props.repo}/`;
let cardId = "";
if (props.type === "snippet") cardId = props.title.replaceAll(" ", "-");
else if (props.type === "theme") cardId = props.manifest.usercss;
else if (props.type === "extension") cardId = props.manifest.main;
this.localStorageKey = `marketplace:installed:${prefix}${cardId}`;
Object.assign(this, props);
// Needs to be after Object.assign so an undefined 'tags' field doesn't overwrite the default []
this.tags = props.tags || [];
if (props.include) this.tags.push("external JS");
this.state = {
// Initial value. Used to trigger a re-render.
// isInstalled() is used for all other intents and purposes
installed: localStorage.getItem(this.localStorageKey) !== null,
// TODO: Can I remove `stars` from `this`? Or maybe just put everything in `state`?
stars: this.stars,
tagsExpanded: false,
};
}
// Using this because it gets the live value ('installed' is stuck after a re-render)
isInstalled() {
return localStorage.getItem(this.localStorageKey) !== null;
}
async componentDidMount() {
// Refresh stars if on "Installed" tab with stars enabled
if (CONFIG.activeTab === "Installed" && CONFIG.visual.stars && this.type !== "snippet") {
// https://docs.github.com/en/rest/reference/repos#get-a-repository
const url = `https://api.github.com/repos/${this.user}/${this.repo}`;
// TODO: This implementation could probably be improved.
// It might have issues when quickly switching between tabs.
const repoData = await fetch(url).then(res => res.json());
if (this.state.stars !== repoData.stargazers_count) {
this.setState({ stars: repoData.stargazers_count }, () => {
console.log(`Stars updated to: ${this.state.stars}; updating localstorage.`);
this.installExtension();
});
}
}
}
buttonClicked() {
if (this.type === "extension") {
if (this.isInstalled()) {
console.log("Extension already installed, removing");
this.removeExtension();
} else {
this.installExtension();
}
openReloadModal();
} else if (this.type === "theme") {
const themeKey = localStorage.getItem("marketplace:theme-installed");
const previousTheme = getLocalStorageDataFromKey(themeKey, {});
console.log(previousTheme);
console.log(themeKey);
if (this.isInstalled()) {
console.log("Theme already installed, removing");
this.removeTheme(this.localStorageKey);
} else {
// Remove theme if already installed, then install the new theme
this.removeTheme();
this.installTheme();
}
// If the new or previous theme has JS, prompt to reload
if (this.include || previousTheme.include) openReloadModal();
} else if (this.type === "snippet") {
if (this.isInstalled()) {
console.log("Snippet already installed, removing");
this.removeSnippet();
} else {
this.installSnippet();
}
} else {
console.error("Unknown card type");
}
}
installExtension() {
console.log(`Installing extension ${this.localStorageKey}`);
// Add to localstorage (this stores a copy of all the card props in the localstorage)
// TODO: refactor/clean this up
localStorage.setItem(this.localStorageKey, JSON.stringify({
manifest: this.manifest,
type: this.type,
title: this.title,
subtitle: this.subtitle,
authors: this.authors,
user: this.user,
repo: this.repo,
branch: this.branch,
imageURL: this.imageURL,
extensionURL: this.extensionURL,
readmeURL: this.readmeURL,
stars: this.state.stars,
}));
// Add to installed list if not there already
const installedExtensions = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedExtensions, []);
if (installedExtensions.indexOf(this.localStorageKey) === -1) {
installedExtensions.push(this.localStorageKey);
localStorage.setItem(LOCALSTORAGE_KEYS.installedExtensions, JSON.stringify(installedExtensions));
}
console.log("Installed");
this.setState({ installed: true });
// console.log(JSON.parse(localStorage.getItem(this.localStorageKey)));
}
removeExtension() {
const extValue = localStorage.getItem(this.localStorageKey);
// console.log(JSON.parse(extValue));
if (extValue) {
console.log(`Removing extension ${this.localStorageKey}`);
// Remove from localstorage
localStorage.removeItem(this.localStorageKey);
// Remove from installed list
const installedExtensions = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedExtensions, []);
const remainingInstalledExtensions = installedExtensions.filter((key) => key !== this.localStorageKey);
localStorage.setItem(LOCALSTORAGE_KEYS.installedExtensions, JSON.stringify(remainingInstalledExtensions));
console.log("Removed");
this.setState({ installed: false });
}
}
async installTheme() {
console.log(`Installing theme ${this.localStorageKey}`);
let parsedSchemes = null;
if (this.schemesURL) {
const schemesResponse = await fetch(this.schemesURL);
const colourSchemes = await schemesResponse.text();
parsedSchemes = parseIni(colourSchemes);
}
console.log(parsedSchemes);
const activeScheme = parsedSchemes ? Object.keys(parsedSchemes)[0] : null;
// Add to localstorage (this stores a copy of all the card props in the localstorage)
// TODO: refactor/clean this up
localStorage.setItem(this.localStorageKey, JSON.stringify({
manifest: this.manifest,
type: this.type,
title: this.title,
subtitle: this.subtitle,
authors: this.authors,
user: this.user,
repo: this.repo,
branch: this.branch,
imageURL: this.imageURL,
extensionURL: this.extensionURL,
readmeURL: this.readmeURL,
stars: this.state.stars,
tags: this.tags,
// Theme stuff
cssURL: this.cssURL,
schemesURL: this.schemesURL,
include: this.include,
// Installed theme localstorage item has schemes, nothing else does
schemes: parsedSchemes,
activeScheme,
}));
// TODO: handle this differently?
// Add to installed list if not there already
const installedThemes = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedThemes, []);
if (installedThemes.indexOf(this.localStorageKey) === -1) {
installedThemes.push(this.localStorageKey);
localStorage.setItem(LOCALSTORAGE_KEYS.installedThemes, JSON.stringify(installedThemes));
// const usercssURL = `https://raw.github.com/${this.user}/${this.repo}/${this.branch}/${this.manifest.usercss}`;
localStorage.setItem(LOCALSTORAGE_KEYS.themeInstalled, this.localStorageKey);
}
console.log("Installed");
// TODO: We'll also need to actually update the usercss etc, not just the colour scheme
// e.g. the stuff from extension.js, like injectUserCSS() etc.
if (!this.include) {
// Add new theme css
this.injectUserCSS(this.localStorageKey);
// Update the active theme in Grid state, triggers state change and re-render
this.updateActiveTheme(this.localStorageKey);
// Update schemes in Grid, triggers state change and re-render
this.updateColourSchemes(parsedSchemes, activeScheme);
}
this.setState({ installed: true });
}
removeTheme(themeKey = null) {
// If don't specify theme, remove the currently installed theme
themeKey = themeKey || localStorage.getItem(LOCALSTORAGE_KEYS.themeInstalled);
const themeValue = themeKey && localStorage.getItem(themeKey);
if (themeValue) {
console.log(`Removing theme ${themeKey}`);
// Remove from localstorage
localStorage.removeItem(themeKey);
// Remove record of installed theme
localStorage.removeItem(LOCALSTORAGE_KEYS.themeInstalled);
// Remove from installed list
const installedThemes = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedThemes, []);
const remainingInstalledThemes = installedThemes.filter((key) => key !== themeKey);
localStorage.setItem(LOCALSTORAGE_KEYS.installedThemes, JSON.stringify(remainingInstalledThemes));
console.log("Removed");
// Removes the current theme CSS
this.injectUserCSS(null);
// Update the active theme in Grid state
this.updateActiveTheme(null);
// Removes the current colour scheme
this.updateColourSchemes(null);
this.setState({ installed: false });
}
}
installSnippet() {
console.log(`Installing snippet ${this.localStorageKey}`);
localStorage.setItem(this.localStorageKey, JSON.stringify({
code: this.code,
title: this.title,
description: this.description,
}));
// Add to installed list if not there already
const installedSnippetKeys = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedSnippets, []);
if (installedSnippetKeys.indexOf(this.localStorageKey) === -1) {
installedSnippetKeys.push(this.localStorageKey);
localStorage.setItem(LOCALSTORAGE_KEYS.installedSnippets, JSON.stringify(installedSnippetKeys));
}
const installedSnippets = installedSnippetKeys.map((key) => getLocalStorageDataFromKey(key));
initializeSnippets(installedSnippets);
this.setState({ installed: true });
}
removeSnippet() {
localStorage.removeItem(this.localStorageKey);
// Remove from installed list
const installedSnippetKeys = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedSnippets, []);
const remainingInstalledSnippetKeys = installedSnippetKeys.filter((key) => key !== this.localStorageKey);
localStorage.setItem(LOCALSTORAGE_KEYS.installedSnippets, JSON.stringify(remainingInstalledSnippetKeys));
const remainingInstalledSnippets = remainingInstalledSnippetKeys.map((key) => getLocalStorageDataFromKey(key));
initializeSnippets(remainingInstalledSnippets);
this.setState({ installed: false });
}
openReadme() {
if (this.manifest && this.manifest.readme) {
Spicetify.Platform.History.push({
pathname: "/spicetify-marketplace/readme",
state: {
data: {
title: this.title,
user: this.user,
repo: this.repo,
branch: this.branch,
readmeURL: this.readmeURL,
},
},
});
} else {
Spicetify.showNotification("No page was found");
}
}
generateAuthorsDiv() {
// Add a div with author links inside
const authorsDiv = (
react.createElement("div", { className: "marketplace-card__authors" },
this.authors.map((author) => {
return (
react.createElement("a", {
title: author.name,
className: "marketplace-card__author",
href: author.url,
draggable: false,
dir: "auto",
target: "_blank",
rel: "noopener noreferrer",
onClick: (e) => e.stopPropagation(),
}, author.name)
);
}),
)
);
return authorsDiv;
}
generateTags(tags) {
return tags.reduce((accum, tag) => {
// Render tags if enabled. Always render external JS tag
if (CONFIG.visual.tags || tag === "external JS") {
accum.push(
react.createElement("li", {
className: "marketplace-card__tag",
draggable: false,
"data-tag": tag,
}, tag),
);
}
return accum;
}, []);
}
generateTagsList() {
const baseTags = this.tags.slice(0, this.MAX_TAGS);
const extraTags = this.tags.slice(this.MAX_TAGS);
// Add a ul with tags inside
const tagsList = (
react.createElement("ul", { className: "marketplace-card__tags" },
this.generateTags(baseTags),
// Show any extra tags if expanded
extraTags.length && this.state.tagsExpanded
? this.generateTags(extraTags)
: null,
)
);
// Render the tags list and add expand button if there are more tags
return [tagsList, extraTags.length && !this.state.tagsExpanded
? react.createElement("button", {
className: "marketplace-card__tags-more-btn",
onClick: (e) => {
e.stopPropagation();
this.setState({ tagsExpanded: true });
},
}, "...")
: null];
}
render() {
// Cache this for performance
let IS_INSTALLED = this.isInstalled();
// console.log(`Rendering ${this.localStorageKey} - is ${IS_INSTALLED ? "" : "not"} installed`);
// Kill the card if it has been uninstalled on the "Installed" tab
// TODO: is this kosher, or is there a better way to handle?
if (CONFIG.activeTab === "Installed" && !IS_INSTALLED) {
console.log("Card item not installed");
return null;
}
const cardClasses = ["main-card-card", `marketplace-card--${this.type}`];
if (IS_INSTALLED) cardClasses.push("marketplace-card--installed");
let detail = [];
// this.visual.type && detail.push(this.type);
if (this.type !== "snippet" && this.visual.stars) {
detail.push(`${this.state.stars}`);
}
return react.createElement(Spicetify.ReactComponent.RightClickMenu || "div", {
menu: react.createElement(this.menuType, {}),
}, react.createElement("div", {
className: cardClasses.join(" "),
onClick: () => this.openReadme(),
}, react.createElement("div", {
className: "main-card-draggable",
draggable: "true",
}, react.createElement("div", {
className: "main-card-imageContainer",
}, react.createElement("div", {
className: "main-cardImage-imageWrapper",
}, react.createElement("div", {
}, react.createElement("img", {
"aria-hidden": "false",
draggable: "false",
loading: "lazy",
src: this.imageURL,
className: "main-image-image main-cardImage-image",
onError: (e) => {
// Set to transparent PNG to remove the placeholder icon
// https://png-pixel.com
e.target.setAttribute("src", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII");
// Add class for styling
e.target.closest(".main-cardImage-imageWrapper").classList.add("main-cardImage-imageWrapper--error");
},
//Create a div using normalized play button classes to use the css provided by themes
})))), react.createElement("div", {
className: "main-card-cardMetadata",
}, react.createElement("a", {
draggable: "false",
title: this.type === "snippet" ? this.props.title : this.manifest.name,
className: "main-cardHeader-link",
dir: "auto",
href: "TODO: add some href here?",
}, react.createElement("div", {
className: "main-cardHeader-text main-type-balladBold",
as: "div",
}, this.props.title)), react.createElement("div", {
className: "main-cardSubHeader-root main-type-mestoBold marketplace-cardSubHeader",
as: "div",
},
// Add authors if they exist
this.authors && this.generateAuthorsDiv(),
react.createElement("span", null, detail.join(" ")),
), react.createElement("p", {
className: "marketplace-card-desc",
}, this.type === "snippet" ? this.props.description : this.manifest.description),
this.tags.length ? react.createElement("div", {
className: "marketplace-card__bottom-meta main-type-mestoBold",
as: "div",
}, this.generateTagsList()) : null,
IS_INSTALLED && react.createElement("div", {
className: "marketplace-card__bottom-meta main-type-mestoBold",
as: "div",
}, "✓ Installed"), react.createElement("div", {
className: "main-card-PlayButtonContainer",
}, react.createElement("button", {
className: "main-playButton-PlayButton main-playButton-primary",
// If it is installed, it will remove it when button is clicked, if not it will save
"aria-label": IS_INSTALLED ? Spicetify.Locale.get("remove") : Spicetify.Locale.get("save"),
style: { "--size": "40px", "cursor": "pointer" },
onClick: (e) => {
e.stopPropagation();
this.buttonClicked();
},
},
//If the extension, theme, or snippet is already installed, it will display trash, otherwise it displays download
IS_INSTALLED ? TRASH_ICON : DOWNLOAD_ICON,
)),
))));
}
// TODO: keep in sync with extension.js
/**
* Update the user.css in the DOM
* @param {string | null} theme The theme localStorageKey or null, if we want to reset the theme
*/
async injectUserCSS(theme) {
try {
// Remove any existing Spicetify user.css
const existingUserThemeCSS = document.querySelector("link[href='user.css']");
if (existingUserThemeCSS) existingUserThemeCSS.remove();
// Remove any existing marketplace scheme
const existingMarketplaceUserCSS = document.querySelector("style.marketplaceCSS.marketplaceUserCSS");
if (existingMarketplaceUserCSS) existingMarketplaceUserCSS.remove();
if (theme) {
const userCSS = await this.parseCSS();
// Add new marketplace scheme
const userCssTag = document.createElement("style");
userCssTag.classList.add("marketplaceCSS");
userCssTag.classList.add("marketplaceUserCSS");
userCssTag.innerHTML = userCSS;
document.head.appendChild(userCssTag);
} else {
// Re-add default user.css
const originalUserThemeCSS = document.createElement("link");
originalUserThemeCSS.setAttribute("rel", "stylesheet");
originalUserThemeCSS.setAttribute("href", "user.css");
originalUserThemeCSS.classList.add("userCSS");
document.head.appendChild(originalUserThemeCSS);
}
} catch (error) {
console.warn(error);
}
}
// TODO: keep in sync with extension.js
async parseCSS() {
const userCssUrl = this.cssURL.indexOf("raw.githubusercontent.com") > -1
// TODO: this should probably be the URL stored in localstorage actually (i.e. put this url in localstorage)
? `https://cdn.jsdelivr.net/gh/${this.user}/${this.repo}@${this.branch}/${this.manifest.usercss}`
: this.cssURL;
// TODO: Make this more versatile
const assetsUrl = userCssUrl.replace("/user.css", "/assets/");
console.log("Parsing CSS: ", userCssUrl);
let css = await fetch(userCssUrl).then(res => res.text());
// console.log("Parsed CSS: ", css);
// @ts-ignore
let urls = css.matchAll(/url\(['|"](?<path>.+?)['|"]\)/gm) || [];
for (const match of urls) {
const url = match.groups.path;
// console.log(url);
// If it's a relative URL, transform it to HTTP URL
if (!url.startsWith("http") && !url.startsWith("data")) {
const newUrl = assetsUrl + url.replace(/\.\//g, "");
css = css.replace(url, newUrl);
}
}
// console.log("New CSS: ", css);
return css;
}
}

View file

@ -0,0 +1,74 @@
// eslint-disable-next-line no-redeclare, no-unused-vars
class LoadingIcon extends react.Component {
render() {
return react.createElement("svg", {
width: "100px", height: "100px", viewBox: "0 0 100 100", preserveAspectRatio: "xMidYMid",
}, react.createElement("circle", {
cx: "50", cy: "50", r: "0", fill: "none", stroke: "currentColor", "stroke-width": "2",
}, react.createElement("animate", {
attributeName: "r", repeatCount: "indefinite", dur: "1s", values: "0;40", keyTimes: "0;1", keySplines: "0 0.2 0.8 1", calcMode: "spline", begin: "0s",
}), react.createElement("animate", {
attributeName: "opacity", repeatCount: "indefinite", dur: "1s", values: "1;0", keyTimes: "0;1", keySplines: "0.2 0 0.8 1", calcMode: "spline", begin: "0s",
})), react.createElement("circle", {
cx: "50", cy: "50", r: "0", fill: "none", stroke: "currentColor", "stroke-width": "2",
}, react.createElement("animate", {
attributeName: "r", repeatCount: "indefinite", dur: "1s", values: "0;40", keyTimes: "0;1", keySplines: "0 0.2 0.8 1", calcMode: "spline", begin: "-0.5s",
}), react.createElement("animate", {
attributeName: "opacity", repeatCount: "indefinite", dur: "1s", values: "1;0", keyTimes: "0;1", keySplines: "0.2 0 0.8 1", calcMode: "spline", begin: "-0.5s",
})));
}
}
// eslint-disable-next-line no-redeclare, no-unused-vars
class LoadMoreIcon extends react.Component {
render() {
return react.createElement("div", {
onClick: this.props.onClick,
}, react.createElement("p", {
style: {
fontSize: 100,
lineHeight: "65px",
},
}, "»"), react.createElement("span", {
style: {
fontSize: 20,
},
}, "Load more"));
}
}
/* eslint-disable no-redeclare, no-unused-vars */
const DOWNLOAD_ICON = react.createElement("svg", {
height: "16",
role: "img",
width: "16",
viewBox: "0 0 512 512",
"aria-hidden": "true",
}, react.createElement("path", {
d: "M480 352h-133.5l-45.25 45.25C289.2 409.3 273.1 416 256 416s-33.16-6.656-45.25-18.75L165.5 352H32c-17.67 0-32 14.33-32 32v96c0 17.67 14.33 32 32 32h448c17.67 0 32-14.33 32-32v-96C512 366.3 497.7 352 480 352zM432 456c-13.2 0-24-10.8-24-24c0-13.2 10.8-24 24-24s24 10.8 24 24C456 445.2 445.2 456 432 456zM233.4 374.6C239.6 380.9 247.8 384 256 384s16.38-3.125 22.62-9.375l128-128c12.49-12.5 12.49-32.75 0-45.25c-12.5-12.5-32.76-12.5-45.25 0L288 274.8V32c0-17.67-14.33-32-32-32C238.3 0 224 14.33 224 32v242.8L150.6 201.4c-12.49-12.5-32.75-12.5-45.25 0c-12.49 12.5-12.49 32.75 0 45.25L233.4 374.6z",
fill: "currentColor",
}));
const SETTINGS_ICON = react.createElement("svg", {
height: "16",
role: "img",
width: "16",
viewBox: "0 0 24 24",
"aria-hidden": "true",
}, react.createElement("path", {
d: "M24 13.616v-3.232c-1.651-.587-2.694-.752-3.219-2.019v-.001c-.527-1.271.1-2.134.847-3.707l-2.285-2.285c-1.561.742-2.433 1.375-3.707.847h-.001c-1.269-.526-1.435-1.576-2.019-3.219h-3.232c-.582 1.635-.749 2.692-2.019 3.219h-.001c-1.271.528-2.132-.098-3.707-.847l-2.285 2.285c.745 1.568 1.375 2.434.847 3.707-.527 1.271-1.584 1.438-3.219 2.02v3.232c1.632.58 2.692.749 3.219 2.019.53 1.282-.114 2.166-.847 3.707l2.285 2.286c1.562-.743 2.434-1.375 3.707-.847h.001c1.27.526 1.436 1.579 2.019 3.219h3.232c.582-1.636.75-2.69 2.027-3.222h.001c1.262-.524 2.12.101 3.698.851l2.285-2.286c-.744-1.563-1.375-2.433-.848-3.706.527-1.271 1.588-1.44 3.221-2.021zm-12 2.384c-2.209 0-4-1.791-4-4s1.791-4 4-4 4 1.791 4 4-1.791 4-4 4z",
fill: "currentColor",
}));
const TRASH_ICON = react.createElement("svg", {
height: "16",
role: "img",
width: "16",
viewBox: "0 0 448 512",
"aria-hidden": "true",
}, react.createElement("path", {
d: "M53.21 467c1.562 24.84 23.02 45 47.9 45h245.8c24.88 0 46.33-20.16 47.9-45L416 128H32L53.21 467zM432 32H320l-11.58-23.16c-2.709-5.42-8.25-8.844-14.31-8.844H153.9c-6.061 0-11.6 3.424-14.31 8.844L128 32H16c-8.836 0-16 7.162-16 16V80c0 8.836 7.164 16 16 16h416c8.838 0 16-7.164 16-16V48C448 39.16 440.8 32 432 32z",
fill: "currentColor",
}));
/* eslint-enable no-redeclare, no-unused-vars */

View file

@ -0,0 +1,73 @@
const OptionsMenuItemIcon = react.createElement("svg", {
width: 16,
height: 16,
viewBox: "0 0 16 16",
fill: "currentColor",
}, react.createElement("path", {
d: "M13.985 2.383L5.127 12.754 1.388 8.375l-.658.77 4.397 5.149 9.618-11.262z",
}));
// @ts-ignore
const OptionsMenuItem = react.memo(({ onSelect, value, isSelected }) => {
return react.createElement(Spicetify.ReactComponent.MenuItem, {
onClick: onSelect,
icon: isSelected ? OptionsMenuItemIcon : null,
}, value);
});
// eslint-disable-next-line no-redeclare, no-unused-vars
const OptionsMenu = react.memo(({
// @ts-ignore
options,
// @ts-ignore
onSelect,
// @ts-ignore
selected,
// @ts-ignore
defaultValue,
// @ts-ignore
bold = false,
}) => {
/**
* <Spicetify.ReactComponent.ContextMenu
* menu = { options.map(a => <OptionsMenuItem>) }
* >
* <button>
* <span> {select.value} </span>
* <svg> arrow icon </svg>
* </button>
* </Spicetify.ReactComponent.ContextMenu>
*/
let menuRef = react.useRef(null);
return react.createElement(Spicetify.ReactComponent.ContextMenu, {
menu: react.createElement(Spicetify.ReactComponent.Menu, {
}, options.map(({ key, value }) => react.createElement(OptionsMenuItem, {
// @ts-ignore
value,
onSelect: () => {
onSelect(key);
// Close menu on item click
menuRef.current?.click();
},
isSelected: selected?.key === key,
})),
),
trigger: "click",
action: "toggle",
renderInline: true,
}, react.createElement("button", {
className: "optionsMenu-dropBox",
ref: menuRef,
},
react.createElement("span", {
className: bold ? "main-type-mestoBold" : "main-type-mesto",
}, selected?.value || defaultValue),
react.createElement("svg", {
height: "16",
width: "16",
fill: "currentColor",
viewBox: "0 0 16 16",
}, react.createElement("path", {
d: "M3 6l5 5.794L13 6z",
}))));
});

View file

@ -0,0 +1,85 @@
// eslint-disable-next-line no-unused-vars, no-redeclare
class ReadmePage extends react.Component {
constructor(props) {
super(props);
Object.assign(this, props);
// TODO: decide what data we want to pass in and how we want to store it
// (this currently comes from Card.openReadme)
/** @type { { title: string; user: string; repo: string; branch: string; readmeURL: string; readmeDir: string; } } */
this.data;
this.state = { html: "<p>Loading...</p>" };
}
componentDidMount() {
// Get and set readme html once loaded
this.getReadmeHTML()
.then((html) => this.setState({ html }));
}
componentDidUpdate() {
// Add error handler in attempt to fix broken image urls
// e.g. "screenshot.png" loads https://xpui.app.spotify.com/screenshot.png and breaks
// so I turn it into https://raw.githubusercontent.com/theRealPadster/spicetify-hide-podcasts/main/screenshot.png
// This works for urls relative to the repo root
document.querySelectorAll("#marketplace-readme img").forEach((img) => {
img.addEventListener("error", (e) => {
// @ts-ignore
const originalSrc = e.target.getAttribute("src");
const fixedSrc = `https://raw.githubusercontent.com/${this.data.user}/${this.data.repo}/${this.data.branch}/${originalSrc}`;
// @ts-ignore
e.target.setAttribute("src", fixedSrc);
}, { once: true });
});
}
render() {
return react.createElement("section", {
className: "contentSpacing",
},
react.createElement("div", {
className: "marketplace-header",
}, react.createElement("h1", null, this.props.title),
),
react.createElement("div", {
id: "marketplace-readme",
className: "marketplace-readme__container",
dangerouslySetInnerHTML: {
__html: this.state.html,
},
}));
}
async getReadmeHTML() {
try {
const readmeTextRes = await fetch(this.data.readmeURL);
if (!readmeTextRes.ok) throw Spicetify.showNotification(`Error loading README (HTTP ${readmeTextRes.status})`);
const readmeText = await readmeTextRes.text();
const postBody = {
text: readmeText,
context: `${this.data.user}/${this.data.repo}`,
mode: "gfm",
};
const readmeHtmlRes = await fetch("https://api.github.com/markdown", {
method: "POST",
body: JSON.stringify(postBody),
});
if (!readmeHtmlRes.ok) throw Spicetify.showNotification(`Error parsing README (HTTP ${readmeHtmlRes.status})`);
const readmeHtml = await readmeHtmlRes.text();
if (readmeHtml == null) {
Spicetify.Platform.History.goBack();
}
return readmeHtml;
} catch (err) {
Spicetify.Platform.History.goBack();
return null;
}
}
}

View file

@ -0,0 +1,58 @@
let reloadContainer;
// const MODAL_SUBTITLE = "Reload needed to complete uninstall";
const MODAL_TITLE = "Reload required";
// eslint-disable-next-line no-unused-vars, no-redeclare
function openReloadModal() {
const triggerModal = () => {
Spicetify.PopupModal.display({
title: MODAL_TITLE,
content: reloadContainer,
});
};
if (reloadContainer) {
triggerModal();
return;
}
reloadContainer = document.createElement("div");
reloadContainer.id = "marketplace-reload-container";
// const optionHeader = document.createElement("h2");
// optionHeader.innerText = MODAL_SUBTITLE;
const paragraph = document.createElement("p");
paragraph.innerText = "A page reload is required to complete this operation.";
const buttonContainer = document.createElement("div");
buttonContainer.classList.add("marketplace-reload-modal__button-container");
const okayBtn = document.createElement("button");
okayBtn.id = "marketplace-reload-okay";
// TODO: add our own classes for styling?
okayBtn.innerText = "Reload now";
okayBtn.classList.add("main-buttons-button", "main-button-secondary", "main-playlistEditDetailsModal-save");
okayBtn.onclick = () => {
Spicetify.PopupModal.hide();
location.reload();
};
const cancelBtn = document.createElement("button");
cancelBtn.id = "marketplace-reload-cancel";
cancelBtn.innerText = "Reload later";
cancelBtn.classList.add("main-buttons-button", "main-button-secondary", "main-playlistEditDetailsModal-save");
cancelBtn.onclick = () => {
Spicetify.PopupModal.hide();
};
buttonContainer.append(okayBtn, cancelBtn);
reloadContainer.append(
paragraph,
buttonContainer,
);
triggerModal();
}

View file

@ -0,0 +1,212 @@
let configContainer;
// eslint-disable-next-line no-unused-vars, no-redeclare
function openConfig() {
const triggerModal = () => {
Spicetify.PopupModal.display({
title: "Marketplace",
content: configContainer,
isLarge: true,
});
};
if (configContainer) {
triggerModal();
return;
}
CONFIG.tabsElement = {};
const optionHeader = document.createElement("h2");
optionHeader.innerText = "Options";
const tabsHeader = document.createElement("h2");
tabsHeader.innerText = "Tabs";
const tabsContainer = document.createElement("div");
function stackTabElements() {
CONFIG.tabs.forEach(({ name }, index) => {
const el = CONFIG.tabsElement[name];
const [ up, down ] = el.querySelectorAll("button");
if (index === 0) {
up.disabled = true;
down.disabled = false;
} else if (index === (CONFIG.tabs.length - 1)) {
up.disabled = false;
down.disabled = true;
} else {
up.disabled = false;
down.disabled = false;
}
tabsContainer.append(el);
});
gridUpdateTabs && gridUpdateTabs();
}
function posCallback(el, dir) {
const id = el.dataset.id;
const curPos = CONFIG.tabs.findIndex(({ name }) => name === id);
const newPos = curPos + dir;
const temp = CONFIG.tabs[newPos];
CONFIG.tabs[newPos] = CONFIG.tabs[curPos];
CONFIG.tabs[curPos] = temp;
localStorage.setItem(
LOCALSTORAGE_KEYS.tabs,
JSON.stringify(CONFIG.tabs),
);
stackTabElements();
}
function toggleCallback(el) {
const id = el.dataset.id;
const slider = el.querySelector("input[type='checkbox']");
// If we're removing the tab, it's not in the enabled tabs list
const toRemove = !slider.checked;
const tabItem = CONFIG.tabs.filter(({ name }) => name === id)[0];
// Enable/disable tab
tabItem.enabled = !toRemove;
// Always "remove" because it re-adds it with the right settings/order in stackTabElements()
CONFIG.tabsElement[id].remove();
// Persist the new enabled tabs
localStorage.setItem(LOCALSTORAGE_KEYS.tabs, JSON.stringify(CONFIG.tabs));
// Refresh
stackTabElements();
}
// Create the tabs settings DOM elements
CONFIG.tabs.forEach(({ name }) => {
CONFIG.tabsElement[name] = createTabOption(
name,
posCallback,
toggleCallback,
);
});
stackTabElements();
configContainer = document.createElement("div");
configContainer.id = "marketplace-config-container";
// Reset Marketplace section
const resetHeader = document.createElement("h2");
resetHeader.innerText = "Reset Marketplace";
const resetContainer = document.createElement("div");
resetContainer.innerHTML = `
<div class="setting-row">
<label class="col description">Uninstall all extensions and themes, and reset preferences</label>
<div class="col action">
<button class="main-buttons-button main-button-secondary">Reset</button>
</div>
</div>`;
const resetBtn = resetContainer.querySelector("button");
resetBtn.onclick = resetMarketplace; // in Utils.js
configContainer.append(
optionHeader,
createToggle("Stars count", "stars"),
createToggle("Tags", "tags"),
createToggle("Hide installed in Marketplace", "hideInstalled"),
createToggle("Shift Colors Every Minute", "colorShift"),
// TODO: add these features maybe?
// createSlider("Followers count", "followers"),
// createSlider("Post type", "type"),
tabsHeader,
tabsContainer,
resetHeader,
resetContainer,
);
triggerModal();
const closeButton = document.querySelector("body > generic-modal button.main-trackCreditsModal-closeBtn");
const modalOverlay = document.querySelector("body > generic-modal > div");
if (closeButton instanceof HTMLElement
&& modalOverlay instanceof HTMLElement) {
closeButton.onclick = () => location.reload();
closeButton.setAttribute("style", "cursor: pointer;");
modalOverlay.onclick = (e) => {
// If clicked on overlay, also reload
if (e.target === modalOverlay) {
location.reload();
}
};
}
}
// Generate the DOM markup for a toggle switch
function renderToggle(enabled, classes) {
return `
<label class="x-toggle-wrapper ${classes ? classes.join(" "): ""}">
<input class="x-toggle-input" type="checkbox" ${enabled ? "checked" : ""}>
<span class="x-toggle-indicatorWrapper">
<span class="x-toggle-indicator"></span>
</span>
</label>
`;
}
function createToggle(name, key) {
const container = document.createElement("div");
container.innerHTML = `
<div class="setting-row">
<label class="col description">${name}</label>
<div class="col action">
${renderToggle(!!CONFIG.visual[key])}
</div>
</div>`;
const slider = container.querySelector("input");
slider.onchange = () => {
const state = slider.checked;
CONFIG.visual[key] = state;
localStorage.setItem(`marketplace:${key}`, String(state));
gridUpdatePostsVisual && gridUpdatePostsVisual();
};
return container;
}
function createTabOption(id, posCallback, toggleCallback) {
const tabItem = CONFIG.tabs.filter(({ name }) => name === id)[0];
const tabEnabled = tabItem.enabled;
const container = document.createElement("div");
container.dataset.id = id;
container.innerHTML = `
<div class="setting-row">
<h3 class="col description">${id}</h3>
<div class="col action">
<button class="arrow-btn">
<svg height="16" width="16" viewBox="0 0 16 16" fill="currentColor">
${Spicetify.SVGIcons["chart-up"]}
</svg>
</button>
<button class="arrow-btn">
<svg height="16" width="16" viewBox="0 0 16 16" fill="currentColor">
${Spicetify.SVGIcons["chart-down"]}
</svg>
</button>
${renderToggle(tabEnabled, id === "Extensions" ? ["disabled"] : [])}
</div>
</div>`;
const [ up, down ] = container.querySelectorAll("button");
const toggle = container.querySelector("input");
up.onclick = () => posCallback(container, -1);
down.onclick = () => posCallback(container, 1);
toggle.onchange = () => toggleCallback(container);
return container;
}

View file

@ -0,0 +1,28 @@
/// <reference path="OptionsMenu.js" />
// eslint-disable-next-line no-redeclare, no-unused-vars
const SortBox = (props) => {
// constructor(props) {
// super(props);
// }
// useEffect(() => {
// setSortBySelected(props.sortBoxOptions.find(props.sortBySelectedFn));
// }, [props.sortBoxOptions]);
// if (this.props.sortBoxOptions.length === 0) return null;
// TODO: need to make sure this works for the main card sorting as well for the colour schemes
// const sortBySelected = this.props.sortBoxOptions.filter(a => a.key === sortConfig.by)[0];
// const [sortBySelected, setSortBySelected] = useState(props.sortBoxOptions.find(props.sortBySelectedFn));
const sortBySelected = props.sortBoxOptions.find(props.sortBySelectedFn);
// console.log(sortBySelected);
return react.createElement("div", {
className: "marketplace-sort-bar",
}, react.createElement("div", {
className: "marketplace-sort-container",
}, react.createElement(OptionsMenu, {
options: props.sortBoxOptions,
onSelect: (value) => props.onChange(value),
selected: sortBySelected,
})));
};

View file

@ -0,0 +1,158 @@
// eslint-disable-next-line no-redeclare, no-unused-vars
class TabBarItem extends react.Component {
/**
* @param {object} props
* @param {object} props.item
* @param {string} props.item.key
* @param {string} props.item.value
* @param {boolean} props.item.active
* @param {boolean} props.item.enabled
* @param {function} props.switchTo
*/
constructor(props) {
super(props);
}
render() {
if (!this.props.item.enabled) return null;
return react.createElement("li", {
className: "marketplace-tabBar-headerItem",
"data-tab": this.props.item.value,
onClick: (event) => {
event.preventDefault();
this.props.switchTo(this.props.item.key);
},
}, react.createElement("a", {
"aria-current": "page",
className: `marketplace-tabBar-headerItemLink ${this.props.item.active ? "marketplace-tabBar-active" : ""}`,
draggable: "false",
href: "",
}, react.createElement("span", {
className: "main-type-mestoBold",
}, this.props.item.value)));
}
}
const TabBarMore = react.memo(({ items, switchTo }) => {
const activeItem = items.find((item) => item.active);
return react.createElement("li", {
className: `marketplace-tabBar-headerItem ${activeItem ? "marketplace-tabBar-active" : ""}`,
}, react.createElement(OptionsMenu, {
options: items,
onSelect: switchTo,
selected: activeItem,
defaultValue: "More",
bold: true,
}));
});
// eslint-disable-next-line no-redeclare, no-unused-vars
const TopBarContent = ({ links, activeLink, switchCallback }) => {
const resizeHost = document.querySelector(".Root__main-view .os-resize-observer-host");
const [windowSize, setWindowSize] = useState(resizeHost.clientWidth);
const resizeHandler = () => setWindowSize(resizeHost.clientWidth);
useEffect(() => {
const observer = new ResizeObserver(resizeHandler);
observer.observe(resizeHost);
return () => {
observer.disconnect();
};
}, [resizeHandler]);
return react.createElement(TabBarContext, null, react.createElement(TabBar, {
className: "queue-queueHistoryTopBar-tabBar",
links,
activeLink,
windowSize,
switchCallback,
}));
};
const TabBarContext = ({ children }) => {
return reactDOM.createPortal(
react.createElement("div", {
className: "main-topBar-topbarContent",
}, children),
document.querySelector(".main-topBar-topbarContentWrapper"),
);
};
const TabBar = react.memo(({ links, activeLink, switchCallback, windowSize = Infinity }) => {
const tabBarRef = react.useRef(null);
const [childrenSizes, setChildrenSizes] = useState([]);
const [availableSpace, setAvailableSpace] = useState(0);
const [droplistItem, setDroplistItems] = useState([]);
// Key is the tab name, value is also the tab name, active is if it's active
const options = links.map(({ name, enabled }) => {
const active = name === activeLink;
return ({ key: name, value: name, active, enabled });
});
useEffect(() => {
if (!tabBarRef.current) return;
setAvailableSpace(tabBarRef.current.clientWidth);
}, [windowSize]);
useEffect(() => {
if (!tabBarRef.current) return;
const children = Array.from(tabBarRef.current.children);
const tabbarItemSizes = children.map(child => child.clientWidth);
setChildrenSizes(tabbarItemSizes);
}, [links]);
useEffect(() => {
if (!tabBarRef.current) return;
const totalSize = childrenSizes.reduce((a, b) => a + b, 0);
// Can we render everything?
if (totalSize <= availableSpace) {
setDroplistItems([]);
return;
}
// The `More` button can be set to _any_ of the children. So we
// reserve space for the largest item instead of always taking
// the last item.
const viewMoreButtonSize = Math.max(...childrenSizes);
// Figure out how many children we can render while also showing
// the More button
const itemsToHide = [];
let stopWidth = viewMoreButtonSize;
childrenSizes.forEach((childWidth, i) => {
if (availableSpace >= stopWidth + childWidth) {
stopWidth += childWidth;
} else {
itemsToHide.push(i);
}
});
setDroplistItems(itemsToHide);
}, [availableSpace, childrenSizes]);
return react.createElement("nav", {
className: "marketplace-tabBar marketplace-tabBar-nav",
}, react.createElement("ul", {
className: "marketplace-tabBar-header",
ref: tabBarRef,
}, options
.filter((_, id) => !droplistItem.includes(id))
.map(item => react.createElement(TabBarItem, {
item,
switchTo: switchCallback,
})),
(droplistItem.length || childrenSizes.length === 0) ?
react.createElement(TabBarMore, {
items: droplistItem.map(i => options[i]).filter(i => i),
switchTo: switchCallback,
}) : null),
);
});

View file

@ -0,0 +1,160 @@
/* eslint-disable no-redeclare, no-unused-vars */
// TODO: Migrate more things to this file
/**
* Convert hexadeciaml string to rgb values
* @param {string} hex 3 or 6 character hex string
* @returns Array of RGB values
*/
const hexToRGB = (hex) => {
if (hex.length === 3) {
hex = hex.split("").map((char) => char + char).join("");
} else if (hex.length != 6) {
throw "Only 3- or 6-digit hex colours are allowed.";
} else if (hex.match(/[^0-9a-f]/i)) {
throw "Only hex colours are allowed.";
}
const aRgbHex = hex.match(/.{1,2}/g);
const aRgb = [
parseInt(aRgbHex[0], 16),
parseInt(aRgbHex[1], 16),
parseInt(aRgbHex[2], 16),
];
return aRgb;
};
/**
* Parse INI file into a colour scheme object
* @param {string} data The INI file string data
* @returns Object containing the parsed colour schemes
*/
const parseIni = (data) => {
const regex = {
section: /^\s*\[\s*([^\]]*)\s*\]\s*$/,
param: /^\s*([^=]+?)\s*=\s*(.*?)\s*$/,
comment: /^\s*;.*$/,
};
let value = {};
let lines = data.split(/[\r\n]+/);
let section = null;
lines.forEach(function(line) {
if (regex.comment.test(line)) {
return;
} else if (regex.param.test(line)) {
let match = line.match(regex.param);
if (section) {
value[section][match[1]] = match[2];
} else {
value[match[1]] = match[2];
}
} else if (regex.section.test(line)) {
let match = line.match(regex.section);
value[match[1]] = {};
section = match[1];
} else if (line.length == 0 && section) {
section = null;
}
});
return value;
};
/**
* Loop through the snippets and add the contents of the code as a style tag in the DOM
* @param { { title: string; description: string; code: string;}[] } snippets The snippets to initialize
*/
// TODO: keep this in sync with the extension.js file
const initializeSnippets = (snippets) => {
// Remove any existing marketplace snippets
const existingSnippets = document.querySelector("style.marketplaceSnippets");
if (existingSnippets) existingSnippets.remove();
const style = document.createElement("style");
const styleContent = snippets.reduce((accum, snippet) => {
accum += `/* ${snippet.title} - ${snippet.description} */\n`;
accum += `${snippet.code}\n`;
return accum;
}, "");
style.innerHTML = styleContent;
style.classList.add("marketplaceSnippets");
document.head.appendChild(style);
};
/**
* Get localStorage data (or fallback value), given a key
* @param {string} key The localStorage key
* @param {any} fallback Fallback value if the key is not found
* @returns The data stored in localStorage, or the fallback value if not found
*/
const getLocalStorageDataFromKey = (key, fallback) => {
return JSON.parse(localStorage.getItem(key)) ?? fallback;
};
/**
* Format an array of authors, given the data from the manifest and the repo owner.
* @param {{ name: string; url: string; }[]} authors Array of authors
* @param {string} user The repo owner
* @returns {{ name: string; url: string; }[]} The authors, with anything missing added
*/
const processAuthors = (authors, user) => {
let parsedAuthors = [];
if (authors && authors.length > 0) {
parsedAuthors = authors;
} else {
parsedAuthors.push({
name: user,
url: "https://github.com/" + user,
});
}
return parsedAuthors;
};
/**
* Generate a list of options for the schemes dropdown.
* @param schemes The schemes object from the theme.
* @returns {{ key: string; value: string; }[]} Array of options for the schemes dropdown.
*/
const generateSchemesOptions = (schemes) => {
// e.g. [ { key: "red", value: "Red" }, { key: "dark", value: "Dark" } ]
if (!schemes) return [];
return Object.keys(schemes).map(schemeName => ({ key: schemeName, value: schemeName }));
};
/**
* It fetches the blacklist.json file from the GitHub repository and returns the array of blocked repos.
* @returns {Promise<string[]>} String array of blacklisted repos
*/
const getBlacklist = async () => {
const url = "https://raw.githubusercontent.com/CharlieS1103/spicetify-marketplace/main/blacklist.json";
const jsonReturned = await fetch(url).then(res => res.json()).catch(() => { });
return jsonReturned.repos;
};
/**
* It fetches the snippets.json file from the Github repository and returns it as a JSON object.
* @returns { Promise<{ title: string; description: string; code: string;}[]> } Array of snippets
*/
const fetchCssSnippets = async () => {
const url = "https://raw.githubusercontent.com/CharlieS1103/spicetify-marketplace/main/snippets.json";
const json = await fetch(url).then(res => res.json()).catch(() => { });
return json;
};
// Reset any Marketplace localStorage keys (effectively resetting it completely)
const resetMarketplace = () => {
console.log("Resetting Marketplace");
// Loop through and reset marketplace keys
Object.keys(localStorage).forEach((key) => {
if (key.startsWith("marketplace:")) {
localStorage.removeItem(key);
console.log(`Removed ${key}`);
}
});
console.log("Marketplace has been reset");
location.reload();
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
// Entire modal container
#marketplace-add-snippet-container {
display: flex;
flex-direction: column;
gap: 16px;
input, textarea {
width: 100%;
margin-bottom: 10px;
padding: 3px 5px;
background-color: var(--spice-main);
color: var(--spice-text);
font-size: 14px;
border: 1px solid var(--spice-button);
border-radius: 4px;
}
}
// Container for label + input
.marketplace-customCSS-input-container {
display: flex;
flex-direction: column;
}
// Submit button
#marketplace-customCSS-submit {
margin-left: auto;
}

View file

@ -0,0 +1,150 @@
// e.g. "Extensions" heading
.marketplace-card-type-heading {
margin: 1em 0 0.5em;
color: var(--spice-subtext);
font-size: 1.3em;
}
.marketplace-grid {
.main-card-draggable {
display: flex;
flex-direction: column;
.main-card-cardMetadata {
flex-grow: 1;
display: flex;
flex-direction: column;
.marketplace-card__author {
// Add comma before authors that have a sibling
& ~ .marketplace-card__author {
&::before {
content: ", ";
}
}
}
ul.marketplace-card__tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
li.marketplace-card__tag {
background-color: var(--spice-tab-active);
border-radius: 4px;
padding: 0px 9px 2px;
&[data-tag='external JS'] {
background-color: hsl(0deg 70% 54%);
color: #fff;
}
&[data-tag='dark'] {
background-color: #000;
color: #fff;
}
&[data-tag='light'] {
background-color: #fff;
color: #333;
}
}
.marketplace-card__tags-more-btn {
background-color: var(--spice-tab-active);
border-radius: 4px;
padding: 0px 9px 2px;
margin-top: 8px;
border: none;
&:hover, &:focus {
filter: brightness(1.4);
}
}
.marketplace-card-desc {
font: -webkit-small-control;
margin: 12px 0;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.marketplace-card__bottom-meta {
margin-top: auto;
margin-bottom: 0;
// Add space between multiple blurbs
& + .marketplace-card__bottom-meta {
margin-top: 8px;
}
}
}
}
&[data-tab='Installed'] {
&:empty::after {
content: 'No installed extensions';
display: block;
}
}
}
// Card preview image error placeholder
.main-cardImage-imageWrapper--error::before {
content: '';
display: block;
position: absolute;
overflow: hidden;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
width: 100%;
background-color: var(--spice-subtext);
filter: brightness(50%);
/* https://developer.mozilla.org/en-US/docs/Web/CSS/clip-path */
/* https://icons8.com/icons/set/box */
clip-path: path('M41.20833,21.5c-2.54758,0.00136 -4.90298,1.35492 -6.18685,3.55534l-12.54167,21.5c-0.64062,1.09578 -0.97875,2.34203 -0.97982,3.61133v86c0,7.83362 6.49972,14.33333 14.33333,14.33333h100.33333c7.83362,0 14.33333,-6.49972 14.33333,-14.33333v-86c-0.00107,-1.2693 -0.3392,-2.51555 -0.97982,-3.61133l-12.54167,-21.5c-1.28387,-2.20042 -3.63926,-3.55398 -6.18685,-3.55534zM45.32357,35.83333h81.35286l8.35645,14.33333h-98.06576zM35.83333,64.5h100.33333v71.66667h-100.33333zM64.5,78.83333v14.33333h43v-14.33333z');
clip-rule: nonzero;
transform: scale(0.5);
}
.main-cardImage-imageWrapper--error {
box-shadow: none;
}
.main-card-card:hover .main-cardImage-imageWrapper--error::before {
filter: brightness(100%);
}
.marketplace-cardSubHeader,
.marketplace-card__bottom-meta {
margin-top: 4px;
white-space: normal;
color: var(--spice-subtext);
}
// author, stars, etc.
.marketplace-cardSubHeader {
display: flex;
flex-direction: column;
gap: 8px;
}
.marketplace-card--snippet {
// Hide image preview for CSS snippets
.main-card-imageContainer { display: none; }
}
.marketplace-card--installed {
flex: 0 !important;
-webkit-box-flex: 0 !important;
border: 1px solid var(--spice-button);
}

View file

@ -0,0 +1,8 @@
/*
TODO: can we get this added to the default Spicetify stylesheet?
It should fix broken extension modals
*/
.GenericModal {
background-color: var(--spice-player);
}

View file

@ -0,0 +1,149 @@
.marketplace-header {
-webkit-box-pack: justify;
-webkit-box-align: center;
align-content: space-between;
align-items: center;
color: var(--spice-text);
display: flex;
justify-content: space-between;
margin: 16px 0;
// To position the settings button + colour schemes
position: sticky;
flex-direction: row-reverse;
top: 80px;
z-index: 1;
h1 {
position: fixed;
left: 16px;
@media (min-width: 1024px) {
left: 32px;
}
}
}
.marketplace-header__right {
display: flex;
}
.marketplace-sort-bar {
align-items: center;
display: flex;
}
.marketplace-sort-container {
position: relative;
display: flex;
}
.marketplace-tabBar-headerItem {
-webkit-app-region: no-drag;
display: inline-block;
pointer-events: auto;
}
.marketplace-tabBar-active {
background-color: var(--spice-tab-active);
border-radius: 4px;
}
.marketplace-tabBar-headerItemLink {
border-radius: 4px;
color: var(--spice-text);
display: inline-block;
margin: 0 8px 0 0;
padding: 8px 16px;
position: relative;
text-decoration: none !important;
cursor: pointer;
}
.marketplace-tabBar-nav {
-webkit-app-region: drag;
pointer-events: none;
width: 100%;
}
.marketplace-tabBar-headerItem .optionsMenu-dropBox {
color: var(--spice-text);
border: 0;
max-width: 150px;
height: 42px;
padding: 0 30px 0 12px;
background-color: initial;
cursor: pointer;
appearance: none;
}
.marketplace-tabBar-headerItem .optionsMenu-dropBox svg {
position: absolute;
margin-left: 8px;
}
.marketplace-settings-button {
border-radius: 4px;
color: var(--spice-text);
display: inline-block;
padding: 10px 14px 6px;
margin-left: 8px;
position: relative;
text-decoration: none !important;
cursor: pointer;
background-color: transparent;
border: none;
background-color: var(--spice-sidebar);
&:hover {
background-color: var(--spice-tab-active);
}
}
// Search bar
.searchbar--bar__wrapper {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-grow: 1;
}
.searchbar-bar {
border: unset;
background-color: var(--spice-sidebar) !important;
border-radius: 5px !important;
padding-left: 10px;
padding-right: 10px;
color: var(--spice-text) !important;
z-index: 1000;
}
.marketplace-sort-container {
// The dropdown menus (sort by)
.optionsMenu-dropBox {
grid-gap: 8px;
align-items: center;
background-color: var(--spice-sidebar);
border-radius: 4px;
display: grid;
grid-template-columns: 1fr 16px;
color: rgba(var(--spice-rgb-text), .7);
border: 0;
height: 32px;
margin-left: 8px;
padding: 0 8px 0 12px;
&:hover {
color: var(--spice-text);
}
}
}
option {
background-color: var(--spice-button);
}
// "Add CSS" button
.marketplace-add-snippet-btn {
position: fixed;
bottom: 32px;
right: 32px;
}

View file

@ -0,0 +1,971 @@
/* Readme pages */
// TODO: This is just copy-pasted water.css (https://watercss.kognise.dev/)
// We probably want to customize and make it use the spicetify colours if possible
// I put custom fixes at the bottom of the file
// TODO: I wanted to get it to just @import the external URL and customize the colours,
// but I'm not sure how to get that working yet
// See here? https://www.npmjs.com/package/postcss-import-url
// @import url(https://cdn.jsdelivr.net/npm/water.css@2/out/water.css);
// (This is just for reference)
.spicetify-root-vars {
--spice-shadow: #1f1525;
--spice-text: #ffffff;
--spice-sidebar: #0a0e14;
--spice-button-disabled: #535353;
--spice-main: #0a0e14;
--spice-misc: #bfbfbf;
--spice-selected-row: #909090;
--spice-subtext: #f0f0f0;
--spice-tab-active: #795b84;
--spice-button: #6f3c89;
--spice-notification: #6f3c89;
--spice-player: #0a0e14;
--spice-notification-error: #e22134;
--spice-card: #6f3c89;
--spice-button-active: #795b84;
}
:root {
// --background-body: #202b38;
--background-body: var(--spice-main);
// TODO: what do i use for this? nothing seems suitable
// --background: #161f27;
// --background: rgba(var(--spice-main), 0.5);
// --background: var(--spice-shadow);
--background: var(--spice-main);
// TODO: idk if this is a good colour choice
// --background-alt: #1a242f;
--background-alt: var(--spice-shadow);
--selection: #1c76c5;
// --text-main: #dbdbdb;
--text-main: var(--spice-subtext);
// --text-bright: #fff;
--text-bright: var(--spice-text);
--text-muted: #a9b1ba;
--links: #41adff;
--focus: #0096bfab;
--border: #526980;
--code: #ffbe85;
--animation-duration: 0.1s;
--button-base: #0c151c;
--button-hover: #040a0f;
--scrollbar-thumb: var(--button-hover);
--scrollbar-thumb-hover: rgb(0, 0, 0);
--form-placeholder: #a9a9a9;
--form-text: #fff;
--variable: #d941e2;
--highlight: #efdb43;
--select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E");
}
#marketplace-readme {
// Allow selecting text
user-select: text;
/*
* Forced dark theme version
*/
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji',
sans-serif;
line-height: 1.4;
max-width: 800px;
margin: 20px auto;
padding: 0 10px;
word-wrap: break-word;
color: #dbdbdb;
color: var(--text-main);
background: #202b38;
background: var(--background-body);
text-rendering: optimizeLegibility;
button {
transition: background-color 0.1s linear, border-color 0.1s linear,
color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease;
transition: background-color var(--animation-duration) linear,
border-color var(--animation-duration) linear,
color var(--animation-duration) linear,
box-shadow var(--animation-duration) linear,
transform var(--animation-duration) ease;
}
input {
transition: background-color 0.1s linear, border-color 0.1s linear,
color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease;
transition: background-color var(--animation-duration) linear,
border-color var(--animation-duration) linear,
color var(--animation-duration) linear,
box-shadow var(--animation-duration) linear,
transform var(--animation-duration) ease;
}
textarea {
transition: background-color 0.1s linear, border-color 0.1s linear,
color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease;
transition: background-color var(--animation-duration) linear,
border-color var(--animation-duration) linear,
color var(--animation-duration) linear,
box-shadow var(--animation-duration) linear,
transform var(--animation-duration) ease;
}
h1 {
font-size: 2.2em;
margin-top: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 12px;
margin-top: 24px;
}
h1 {
color: #fff;
color: var(--text-bright);
}
h2 {
color: #fff;
color: var(--text-bright);
}
h3 {
color: #fff;
color: var(--text-bright);
}
h4 {
color: #fff;
color: var(--text-bright);
}
h5 {
color: #fff;
color: var(--text-bright);
}
h6 {
color: #fff;
color: var(--text-bright);
}
strong {
color: #fff;
color: var(--text-bright);
}
h1,
h2,
h3,
h4,
h5,
h6,
b,
strong,
th {
font-weight: 600;
}
q::before {
content: none;
}
q::after {
content: none;
}
blockquote {
border-left: 4px solid #0096bfab;
border-left: 4px solid var(--focus);
margin: 1.5em 0;
padding: 0.5em 1em;
font-style: italic;
}
q {
border-left: 4px solid #0096bfab;
border-left: 4px solid var(--focus);
margin: 1.5em 0;
padding: 0.5em 1em;
font-style: italic;
}
blockquote > footer {
font-style: normal;
border: 0;
}
blockquote cite {
font-style: normal;
}
address {
font-style: normal;
}
a[href^='mailto\:']::before {
content: '📧 ';
}
a[href^='tel\:']::before {
content: '📞 ';
}
a[href^='sms\:']::before {
content: '💬 ';
}
mark {
background-color: #efdb43;
background-color: var(--highlight);
border-radius: 2px;
padding: 0 2px 0 2px;
color: #000;
}
a > code,
a > strong {
color: inherit;
}
button,
select,
input[type='submit'],
input[type='reset'],
input[type='button'],
input[type='checkbox'],
input[type='range'],
input[type='radio'] {
cursor: pointer;
}
input,
select {
display: block;
}
[type='checkbox'],
[type='radio'] {
display: initial;
}
input {
color: #fff;
color: var(--form-text);
background-color: #161f27;
background-color: var(--background);
font-family: inherit;
font-size: inherit;
margin-right: 6px;
margin-bottom: 6px;
padding: 10px;
border: none;
border-radius: 6px;
outline: none;
}
button {
color: #fff;
color: var(--form-text);
background-color: #161f27;
background-color: var(--background);
font-family: inherit;
font-size: inherit;
margin-right: 6px;
margin-bottom: 6px;
padding: 10px;
border: none;
border-radius: 6px;
outline: none;
}
textarea {
color: #fff;
color: var(--form-text);
background-color: #161f27;
background-color: var(--background);
font-family: inherit;
font-size: inherit;
margin-right: 6px;
margin-bottom: 6px;
padding: 10px;
border: none;
border-radius: 6px;
outline: none;
}
select {
color: #fff;
color: var(--form-text);
background-color: #161f27;
background-color: var(--background);
font-family: inherit;
font-size: inherit;
margin-right: 6px;
margin-bottom: 6px;
padding: 10px;
border: none;
border-radius: 6px;
outline: none;
}
button {
background-color: #0c151c;
background-color: var(--button-base);
padding-right: 30px;
padding-left: 30px;
}
input[type='submit'] {
background-color: #0c151c;
background-color: var(--button-base);
padding-right: 30px;
padding-left: 30px;
}
input[type='reset'] {
background-color: #0c151c;
background-color: var(--button-base);
padding-right: 30px;
padding-left: 30px;
}
input[type='button'] {
background-color: #0c151c;
background-color: var(--button-base);
padding-right: 30px;
padding-left: 30px;
}
button:hover {
background: #040a0f;
background: var(--button-hover);
}
input[type='submit']:hover {
background: #040a0f;
background: var(--button-hover);
}
input[type='reset']:hover {
background: #040a0f;
background: var(--button-hover);
}
input[type='button']:hover {
background: #040a0f;
background: var(--button-hover);
}
input[type='color'] {
min-height: 2rem;
padding: 8px;
cursor: pointer;
}
input[type='checkbox'],
input[type='radio'] {
height: 1em;
width: 1em;
}
input[type='radio'] {
border-radius: 100%;
}
input {
vertical-align: top;
}
label {
vertical-align: middle;
margin-bottom: 4px;
display: inline-block;
}
input:not([type='checkbox']):not([type='radio']),
input[type='range'],
select,
button,
textarea {
-webkit-appearance: none;
}
textarea {
display: block;
margin-right: 0;
box-sizing: border-box;
resize: vertical;
}
textarea:not([cols]) {
width: 100%;
}
textarea:not([rows]) {
min-height: 40px;
height: 140px;
}
select {
background: #161f27
url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E")
calc(100% - 12px) 50% / 12px no-repeat;
background: var(--background) var(--select-arrow) calc(100% - 12px) 50% /
12px no-repeat;
padding-right: 35px;
}
select::-ms-expand {
display: none;
}
select[multiple] {
padding-right: 10px;
background-image: none;
overflow-y: auto;
}
input:focus {
box-shadow: 0 0 0 2px #0096bfab;
box-shadow: 0 0 0 2px var(--focus);
}
select:focus {
box-shadow: 0 0 0 2px #0096bfab;
box-shadow: 0 0 0 2px var(--focus);
}
button:focus {
box-shadow: 0 0 0 2px #0096bfab;
box-shadow: 0 0 0 2px var(--focus);
}
textarea:focus {
box-shadow: 0 0 0 2px #0096bfab;
box-shadow: 0 0 0 2px var(--focus);
}
input[type='checkbox']:active,
input[type='radio']:active,
input[type='submit']:active,
input[type='reset']:active,
input[type='button']:active,
input[type='range']:active,
button:active {
transform: translateY(2px);
}
input:disabled,
select:disabled,
button:disabled,
textarea:disabled {
cursor: not-allowed;
opacity: 0.5;
}
::-moz-placeholder {
color: #a9a9a9;
color: var(--form-placeholder);
}
:-ms-input-placeholder {
color: #a9a9a9;
color: var(--form-placeholder);
}
::-ms-input-placeholder {
color: #a9a9a9;
color: var(--form-placeholder);
}
::placeholder {
color: #a9a9a9;
color: var(--form-placeholder);
}
fieldset {
border: 1px #0096bfab solid;
border: 1px var(--focus) solid;
border-radius: 6px;
margin: 0;
margin-bottom: 12px;
padding: 10px;
}
legend {
font-size: 0.9em;
font-weight: 600;
}
input[type='range'] {
margin: 10px 0;
padding: 10px 0;
background: transparent;
}
input[type='range']:focus {
outline: none;
}
input[type='range']::-webkit-slider-runnable-track {
width: 100%;
height: 9.5px;
-webkit-transition: 0.2s;
transition: 0.2s;
background: #161f27;
background: var(--background);
border-radius: 3px;
}
input[type='range']::-webkit-slider-thumb {
box-shadow: 0 1px 1px #000, 0 0 1px #0d0d0d;
height: 20px;
width: 20px;
border-radius: 50%;
background: #526980;
background: var(--border);
-webkit-appearance: none;
margin-top: -7px;
}
input[type='range']:focus::-webkit-slider-runnable-track {
background: #161f27;
background: var(--background);
}
input[type='range']::-moz-range-track {
width: 100%;
height: 9.5px;
-moz-transition: 0.2s;
transition: 0.2s;
background: #161f27;
background: var(--background);
border-radius: 3px;
}
input[type='range']::-moz-range-thumb {
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
height: 20px;
width: 20px;
border-radius: 50%;
background: #526980;
background: var(--border);
}
input[type='range']::-ms-track {
width: 100%;
height: 9.5px;
background: transparent;
border-color: transparent;
border-width: 16px 0;
color: transparent;
}
input[type='range']::-ms-fill-lower {
background: #161f27;
background: var(--background);
border: 0.2px solid #010101;
border-radius: 3px;
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
}
input[type='range']::-ms-fill-upper {
background: #161f27;
background: var(--background);
border: 0.2px solid #010101;
border-radius: 3px;
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
}
input[type='range']::-ms-thumb {
box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d;
border: 1px solid #000;
height: 20px;
width: 20px;
border-radius: 50%;
background: #526980;
background: var(--border);
}
input[type='range']:focus::-ms-fill-lower {
background: #161f27;
background: var(--background);
}
input[type='range']:focus::-ms-fill-upper {
background: #161f27;
background: var(--background);
}
a {
text-decoration: none;
color: #41adff;
color: var(--links);
}
a:hover {
text-decoration: underline;
}
code {
background: #161f27;
background: var(--background);
color: #ffbe85;
color: var(--code);
padding: 2.5px 5px;
border-radius: 6px;
font-size: 1em;
}
samp {
background: #161f27;
background: var(--background);
color: #ffbe85;
color: var(--code);
padding: 2.5px 5px;
border-radius: 6px;
font-size: 1em;
}
time {
background: #161f27;
background: var(--background);
color: #ffbe85;
color: var(--code);
padding: 2.5px 5px;
border-radius: 6px;
font-size: 1em;
}
pre > code {
padding: 10px;
display: block;
overflow-x: auto;
}
var {
color: #d941e2;
color: var(--variable);
font-style: normal;
font-family: monospace;
}
kbd {
background: #161f27;
background: var(--background);
border: 1px solid #526980;
border: 1px solid var(--border);
border-radius: 2px;
color: #dbdbdb;
color: var(--text-main);
padding: 2px 4px 2px 4px;
}
img,
video {
max-width: 100%;
height: auto;
}
hr {
border: none;
border-top: 1px solid #526980;
border-top: 1px solid var(--border);
}
table {
border-collapse: collapse;
margin-bottom: 10px;
width: 100%;
table-layout: fixed;
}
table caption {
text-align: left;
}
td,
th {
padding: 6px;
text-align: left;
vertical-align: top;
word-wrap: break-word;
}
thead {
border-bottom: 1px solid #526980;
border-bottom: 1px solid var(--border);
}
tfoot {
border-top: 1px solid #526980;
border-top: 1px solid var(--border);
}
tbody tr:nth-child(even) {
background-color: #161f27;
background-color: var(--background);
}
tbody tr:nth-child(even) button {
background-color: #1a242f;
background-color: var(--background-alt);
}
tbody tr:nth-child(even) button:hover {
background-color: #202b38;
background-color: var(--background-body);
}
::-webkit-scrollbar {
height: 10px;
width: 10px;
}
::-webkit-scrollbar-track {
background: #161f27;
background: var(--background);
border-radius: 6px;
}
::-webkit-scrollbar-thumb {
background: #040a0f;
background: var(--scrollbar-thumb);
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: rgb(0, 0, 0);
background: var(--scrollbar-thumb-hover);
}
::-moz-selection {
background-color: #1c76c5;
background-color: var(--selection);
color: #fff;
color: var(--text-bright);
}
::selection {
background-color: #1c76c5;
background-color: var(--selection);
color: #fff;
color: var(--text-bright);
}
details {
display: flex;
flex-direction: column;
align-items: flex-start;
background-color: #1a242f;
background-color: var(--background-alt);
padding: 10px 10px 0;
margin: 1em 0;
border-radius: 6px;
overflow: hidden;
}
details[open] {
padding: 10px;
}
details > :last-child {
margin-bottom: 0;
}
details[open] summary {
margin-bottom: 10px;
}
summary {
display: list-item;
background-color: #161f27;
background-color: var(--background);
padding: 10px;
margin: -10px -10px 0;
cursor: pointer;
outline: none;
}
summary:hover,
summary:focus {
text-decoration: underline;
}
details > :not(summary) {
margin-top: 0;
}
summary::-webkit-details-marker {
color: #dbdbdb;
color: var(--text-main);
}
dialog {
background-color: #1a242f;
background-color: var(--background-alt);
color: #dbdbdb;
color: var(--text-main);
border: none;
border-radius: 6px;
border-color: #526980;
border-color: var(--border);
padding: 10px 30px;
}
dialog > header:first-child {
background-color: #161f27;
background-color: var(--background);
border-radius: 6px 6px 0 0;
margin: -10px -30px 10px;
padding: 10px;
text-align: center;
}
dialog::-webkit-backdrop {
background: #0000009c;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
}
dialog::backdrop {
background: #0000009c;
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
}
footer {
border-top: 1px solid #526980;
border-top: 1px solid var(--border);
padding-top: 10px;
color: #a9b1ba;
color: var(--text-muted);
}
body > footer {
margin-top: 40px;
}
@media print {
body,
pre,
code,
summary,
details,
button,
input,
textarea {
background-color: #fff;
}
button,
input,
textarea {
border: 1px solid #000;
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
pre,
code,
button,
input,
textarea,
footer,
summary,
strong {
color: #000;
}
summary::marker {
color: #000;
}
summary::-webkit-details-marker {
color: #000;
}
tbody tr:nth-child(even) {
background-color: #f2f2f2;
}
a {
color: #00f;
text-decoration: underline;
}
}
// === Custom tweaks to water.css ===
code {
// TODO: I added this myself to try and make the code blocks look presentable
// since I don't havea good background colour
border: 1px solid var(--spice-button-disabled);
}
// Add some basic styling (Spotify has it removed)
ul, li {
list-style-position: inside;
li {
margin-bottom: 8px;
}
}
ul { list-style-type: disc; }
ol { list-style-type: auto; }
// Add some basic margin
p, pre, code {
margin: 8px 0;
}
// Make single-line code blocks not trail offscreen
pre {
overflow-x: auto;
}
table {
// Stop install directions "platform/path" stuff from doing the widths equally all the time
table-layout: auto;
th, td {
// Add borders to table cells
border: 1px solid var(--spice-button-disabled);
}
}
}

View file

@ -0,0 +1,14 @@
/* Need to hijack height because .GenericModal has it set to 90% via style attr... */
.GenericModal[aria-label="Reload required"] {
height: 240px !important;
}
.marketplace-reload-modal__button-container {
display: flex;
justify-content: center;
padding-top: 18px;
}
.marketplace-reload-modal__button-container button {
margin: 18px;
padding: 8px 24px;
}

View file

@ -0,0 +1,78 @@
// Make settings modal use same background colour as the reload modal
// (The large style modal uses --spice-player as the background colour by default)
.GenericModal[aria-label="Marketplace"] {
.main-embedWidgetGenerator-container {
background-color: var(--spice-tab-active);
}
// Toggle switches
.x-toggle-wrapper {
cursor: pointer;
margin-inline-start: 12px;
// Hide disabled toggle rows (i.e. the "Extensions" one)
&.disabled {
opacity: 0;
pointer-events: none;
}
}
}
.setting-row {
&::after {
content: "";
display: table;
clear: both;
}
.col {
display: flex;
padding: 10px 0;
align-items: center;
&.description {
float: left;
padding-right: 15px;
cursor: default;
}
&.action {
float: right;
text-align: right;
}
}
}
// The up/down arrows
button.arrow-btn {
align-items: center;
border: 0px;
border-radius: 50%;
background-color: rgba(var(--spice-rgb-shadow), .7);
color: var(--spice-text);
cursor: pointer;
display: flex;
margin-inline-start: 12px;
padding: 8px;
&.small {
width: 24px;
height: 24px;
padding: 5px;
}
&.disabled,
&[disabled] {
color: rgba(var(--spice-rgb-text), .3);
}
}
// Add snippet modal
#marketplace-add-snippet-container input, textarea {
width: 100%;
margin-bottom: 10px;
padding: 3px 5px;
background-color: var(--spice-main);
color: var(--spice-text);
font-size: 14px;
border: 1px solid var(--spice-button);
border-radius: 4px;
}

View file

@ -0,0 +1,15 @@
// This is the main file that imports all the components
/*
TODOs:
- We might want to customize/scope the css classes etc
so we don't interfere with other extension like reddit
*/
@import url(components/grid.scss);
@import url(components/card.scss);
@import url(components/settings.scss);
@import url(components/reload-modal.scss);
@import url(components/add-snippet-modal.scss);
@import url(components/readme-pages.scss);
@import url(components/fixes.scss);

View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2018",
"checkJs": true,
"allowJs": true,
"noEmit": true,
// "typeRoots": ["@types", "../spicetify-cli"],
// "skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
// "strict": true,
"importHelpers": true,
},
"include": ["index.js"],
"exclude": ["node_modules"]
}