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:
parent
ebbd385eb3
commit
f04977817c
529 changed files with 370636 additions and 0 deletions
|
@ -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)!
|
|
@ -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.
|
178
dot_config/spicetify/CustomApps/spicetify-marketplace/README.md
Normal file
178
dot_config/spicetify/CustomApps/spicetify-marketplace/README.md
Normal 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
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"repos": [
|
||||
"https://github.com/Darkempire78/Spotify-Customizer"
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ✏️ Spicetify Docs
|
||||
url: https://spicetify.app/
|
||||
about: Check out our documentation here.
|
|
@ -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.
|
|
@ -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
|
|
@ -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"]
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
dump.rdb
|
||||
.DS_Store
|
||||
.parcel-cache/
|
||||
*.map
|
|
@ -0,0 +1 @@
|
|||
16.10.0
|
|
@ -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
|
|
@ -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));
|
||||
}
|
838
dot_config/spicetify/CustomApps/spicetify-marketplace/index.js
Normal file
838
dot_config/spicetify/CustomApps/spicetify-marketplace/index.js
Normal 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));
|
||||
});
|
||||
}
|
|
@ -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"
|
|
@ -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"
|
||||
]
|
||||
}
|
7533
dot_config/spicetify/CustomApps/spicetify-marketplace/package-lock.json
generated
Normal file
7533
dot_config/spicetify/CustomApps/spicetify-marketplace/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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; }"
|
||||
}
|
||||
]
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 */
|
|
@ -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",
|
||||
}))));
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
})));
|
||||
};
|
|
@ -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),
|
||||
);
|
||||
});
|
|
@ -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();
|
||||
};
|
1149
dot_config/spicetify/CustomApps/spicetify-marketplace/style.css
Normal file
1149
dot_config/spicetify/CustomApps/spicetify-marketplace/style.css
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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"]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue