1
0
Fork 0

Clean up spicetify folder

This commit is contained in:
Florian RICHER 2022-05-09 01:02:49 +02:00
parent 4bcc1690d9
commit befab520ee
524 changed files with 0 additions and 370144 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,160 +0,0 @@
/* 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();
};