Clean up spicetify folder
This commit is contained in:
parent
4bcc1690d9
commit
befab520ee
524 changed files with 0 additions and 370144 deletions
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 */
|
|
@ -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",
|
||||
}))));
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
})));
|
||||
};
|
|
@ -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),
|
||||
);
|
||||
});
|
|
@ -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();
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue