Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use default spotify color fetching #401

Merged
merged 8 commits into from
Dec 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"husky": "^8.0.2",
"i18next": "^22.0.6",
"i18next-browser-languagedetector": "^7.0.1",
"node-vibrant": "3.1.4",
"prismjs": "^1.29.0",
"react-dropdown": "^1.11.0",
"react-i18next": "^12.0.0",
Expand Down
1 change: 1 addition & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class App extends React.Component<{
themeDevTools: JSON.parse(getLocalStorageDataFromKey("marketplace:themeDevTools", false)),
albumArtBasedColors: JSON.parse(getLocalStorageDataFromKey("marketplace:albumArtBasedColors", false)),
albumArtBasedColorsMode: getLocalStorageDataFromKey("marketplace:albumArtBasedColorsMode") || "monochrome-light",
albumArtBasedColorsVibrancy: getLocalStorageDataFromKey("marketplace:albumArtBasedColorsVibrancy") || "PROMINENT",
// Legacy from reddit app
type: JSON.parse(getLocalStorageDataFromKey("marketplace:type", false)),
// I was considering adding watchers as "followers" but it looks like the value is a duplicate
Expand Down
4 changes: 2 additions & 2 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type ButtonType = "round" | "circle";
const Button = (props: {
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
classes?: string[];
label?: string;
label?: string | null;
type?: ButtonType;
children: React.ReactNode;
disabled?: boolean;
Expand All @@ -21,7 +21,7 @@ const Button = (props: {
if (props.classes) classList.push(...props.classes);

return (
<button className={classList.join(" ")} onClick={props.onClick} aria-label={props.label} disabled={props.disabled}>
<button className={classList.join(" ")} onClick={props.onClick} aria-label={props.label || ""} disabled={props.disabled}>
{props.children}
</button>
);
Expand Down
9 changes: 9 additions & 0 deletions src/components/Icons/TooltipIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from "react";

const TooltipIcon = () => {
return (
<svg role="img" height="16" width="16" className="Svg-sc-ytk21e-0 uPxdw nW1RKQOkzcJcX6aDCZB4" viewBox="0 0 16 16"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8z"></path><path d="M7.25 12.026v-1.5h1.5v1.5h-1.5zm.884-7.096A1.125 1.125 0 007.06 6.39l-1.431.448a2.625 2.625 0 115.13-.784c0 .54-.156 1.015-.503 1.488-.3.408-.7.652-.973.818l-.112.068c-.185.116-.26.203-.302.283-.046.087-.097.245-.097.57h-1.5c0-.47.072-.898.274-1.277.206-.385.507-.645.827-.846l.147-.092c.285-.177.413-.257.526-.41.169-.23.213-.397.213-.602 0-.622-.503-1.125-1.125-1.125z"></path></svg>
);
};

export default TooltipIcon;
55 changes: 42 additions & 13 deletions src/components/Modals/Settings/ConfigRow.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from "react";
import { Config } from "../../../types/marketplace-types";

import Toggle from "../../Toggle";
import SortBox from "../../Sortbox";
import TooltipIcon from "../../Icons/TooltipIcon";
const Spicetify = window.Spicetify;

const ConfigRow = (props: {
name: string;
storageKey: string;
Expand All @@ -11,6 +13,7 @@ const ConfigRow = (props: {
updateConfig: (CONFIG: Config) => void;
type?: string;
options?: string[];
description?: string | null;
}) => {
const type = props.type;
const componentId = (type === "dropdown")
Expand All @@ -36,21 +39,47 @@ const ConfigRow = (props: {
localStorage.setItem(`marketplace:${storageKey}`, String(state));
props.updateConfig(props.modalConfig);
};
if (props.description === undefined || props.description === null) {
props.description = "" as string;
}

if (type === "dropdown" && props.options) {
return (
<SortBox
sortBoxOptions={props.options.map((option) => {
return {
key: option,
value: option,
};
})}
onChange={(value) => settingsDropdownChange(value)}
sortBySelectedFn={(item) => {
return item.key == props.modalConfig.visual[props.storageKey];
}}
/>
<div className='setting-row'>
<label htmlFor={componentId} className='col description'>{props.name}</label>
<div className='col action'>
<SortBox
sortBoxOptions={props.options.map((option) => {
return {
key: option,
value: option,
};
})}
onChange={(value) => settingsDropdownChange(value)}
sortBySelectedFn={(item) => {
return item.key == props.modalConfig.visual[props.storageKey];
}}
/>
<Spicetify.ReactComponent.TooltipWrapper
label={
<>
{props.description.split("\n").map(line => {
return <>{line}<br /></>;
})}
</>
}
renderInline={true}
showDelay={10}
placement="top"
labelClassName="marketplace-settings-tooltip"
disabled={false}
>
<div className="marketplace-tooltip-icon">
<TooltipIcon />
</div>
</Spicetify.ReactComponent.TooltipWrapper>
</div>
</div>

);
}
Expand Down
11 changes: 7 additions & 4 deletions src/components/Modals/Settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import React from "react";
import { t } from "i18next";
import { Config } from "../../../types/marketplace-types";

import { resetMarketplace, sleep } from "../../../logic/Utils";
import { getLocalStorageDataFromKey, resetMarketplace, sleep } from "../../../logic/Utils";

import ConfigRow from "./ConfigRow";
import Button from "../../Button";
import TabRow from "./TabRow";

import { openModal } from "../../../logic/LaunchModals";
import { LOCALSTORAGE_KEYS } from "../../../constants";

interface Props {
CONFIG: Config;
Expand Down Expand Up @@ -39,6 +41,9 @@ const SettingsModal = ({ CONFIG, updateAppConfig } : Props) => {
}
};
}
const AlbumArtColorDropDowns = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.albumArtBasedColor) ? <>
<ConfigRow name={t("settings.albumArtBasedColorsMode")} storageKey='albumArtBasedColorsMode' modalConfig={modalConfig} updateConfig={updateConfig} type="dropdown" options={["monochromeDark", "monochromeLight", "analogicComplement", "analogic", "triad", "quad"]} description={t("settings.almbumArtColorsModeToolTip")} />
<ConfigRow name={t("settings.albumArtBasedColorsVibrancy")} storageKey='albumArtBasedColorsVibrancy' modalConfig={modalConfig} updateConfig={updateConfig} type="dropdown" options={["desaturated", "lightVibrant", "prominent", "vibrant"]} description={t("settings.albumArtBasedColorsVibrancyToolTip")} /></> : null;

return (
<div id="marketplace-config-container">
Expand All @@ -49,9 +54,7 @@ const SettingsModal = ({ CONFIG, updateAppConfig } : Props) => {
<ConfigRow name={t("settings.hideInstalledLabel")} storageKey='hideInstalled' modalConfig={modalConfig} updateConfig={updateConfig}/>
<ConfigRow name={t("settings.colourShiftLabel")} storageKey='colorShift' modalConfig={modalConfig} updateConfig={updateConfig}/>
<ConfigRow name={t("settings.albumArtBasedColors")} storageKey='albumArtBasedColors' modalConfig={modalConfig} updateConfig={updateConfig}/>
{/* Make options monochrome-dark ,monochrome-light ,analogic complement ,analogic-complement ,triad ,quad*/}
<ConfigRow name={t("settings.albumArtBasedColorsMode")} storageKey='albumArtBasedColorsMode' modalConfig=
{modalConfig} updateConfig={updateConfig} type="dropdown" options={["monochromeDark", "monochromeLight", "analogicComplement", "analogic", "triad", "quad"]} />
{AlbumArtColorDropDowns}
<h2>{t("settings.tabsHeading")}</h2>
<div className="tabs-container">
{modalConfig.tabs.map(({ name }, index) => {
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const LOCALSTORAGE_KEYS = {
themeInstalled: "marketplace:theme-installed",
albumArtBasedColor: "marketplace:albumArtBasedColors",
albumArtBasedColorMode: "marketplace:albumArtBasedColorsMode",
albumArtBasedColorVibrancy: "marketplace:albumArtBasedColorsVibrancy",
colorShift: "marketplace:colorShift",
};

Expand Down
42 changes: 16 additions & 26 deletions src/logic/Utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { CardProps } from "../components/Card/Card";
import { Author, CardItem, ColourScheme, SchemeIni, Snippet, SortBoxOption } from "../types/marketplace-types";
import Vibrant from "node-vibrant";
import Chroma from "chroma-js";
import { LOCALSTORAGE_KEYS } from "../constants";
/**
Expand Down Expand Up @@ -307,25 +306,16 @@ export const initColorShiftLoop = (schemes: SchemeIni) => {
}, 60 * 1000);
};

export const getColorFromImage = async (image: HTMLImageElement, numColors: number) => {
const swatches = await Vibrant.from(image).maxColorCount(numColors).getPalette((err, palette) => {
if (err) {
console.error(err);
return;
}
return palette;
});

if (swatches.Vibrant) {
// remove the # from the hex
return swatches.Vibrant.hex.substring(1);
}

return "null";
export const getColorFromImage = async (image: string) => {
let vibrancy = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.albumArtBasedColorVibrancy);
// Add a underscore before any uppercase characters, then make the whole string uppercase
vibrancy = vibrancy.replace(/([A-Z])/g, "_$1").toUpperCase();
const colorOptions = (await Spicetify.colorExtractor(image));
const color = colorOptions[vibrancy];
return color.substring(1);
};

export const generateColorPalette = async (mainColor: string, numColors: number) => {
// Generate a palette from https://www.thecolorapi.com/id?hex=0047AB&rgb=0,71,171&hsl=215,100%,34%&cmyk=100,58,0,33&format=html
const mode = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.albumArtBasedColorMode);
// Add a hyphen before any uppercase characters
const modeStr = mode.replace(/([A-Z])/g, "-$1").toLowerCase();
Expand All @@ -337,13 +327,13 @@ export const generateColorPalette = async (mainColor: string, numColors: number)
return colorArray;
};

async function waitForAlbumArt(): Promise<HTMLImageElement | null> {
async function waitForAlbumArt(): Promise<string | undefined> {
// Only return when the album art is loaded
return new Promise((resolve) => {
setInterval(() => {
const albumArt: HTMLImageElement | null = document.querySelector(".main-image-image.cover-art-image");
if (albumArt) {
resolve(albumArt);
const albumArtSrc = Spicetify.Player.data?.track?.metadata?.image_xlarge_url;
if (albumArtSrc) {
resolve(albumArtSrc);
}
}, 50);
});
Expand All @@ -354,16 +344,16 @@ export const initAlbumArtBasedColor = (scheme: ColourScheme) => {
// and update the color scheme accordingly
Spicetify.Player.addEventListener("songchange", async () => {
await sleep(1000);
let albumArt: HTMLImageElement | null = document.querySelector(".main-image-image.cover-art-image");
let albumArtSrc = Spicetify.Player.data?.track?.metadata?.image_xlarge_url;

// If it doesn't exist, wait for it to load
if (albumArt == null || !albumArt.complete) {
albumArt = await waitForAlbumArt();
if (albumArtSrc == null) {
albumArtSrc = await waitForAlbumArt();
}

if (albumArt) {
if (albumArtSrc) {
const numColors = new Set(Object.values(scheme)).size;
const mainColor = await getColorFromImage(albumArt, numColors);
const mainColor: string = await getColorFromImage(albumArtSrc);
const newColors = await generateColorPalette(mainColor, numColors);
/* Find which keys share the same value in the current scheme, create a new scheme that has the value as the key and all the keys in the old scheme as the value
i.e.
Expand Down
5 changes: 4 additions & 1 deletion src/resources/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"settings": {
"colourShiftLabel": "Shift colors every minute",
"albumArtBasedColors": "Change colors based on album art",
"albumArtBasedColorsMode": "The mode that the colorapi uses to generate color schemes"
"albumArtBasedColorsMode": "Color scheme (ColorApi) mode",
"albumArtBasedColorsVibrancy": "Color grabbed from album art",
"albumArtBasedColorsVibrancyToolTip": "Desaturated: The color that is the most prominent but with much less brightness \n Light Vibrant: The most Vibrant color but with the brightness amped up a tad \n Prominent: The color that pops the most in the album art \n Vibrant: The most vibrant color in the album art",
"almbumArtColorsModeToolTip": "Monochrome Dark: A color scheme based directly on the main color selected, using different shades of the main color and mixing in greys to create a color scheme, this is the inverse of Monochrome Light. \n Monochrome Light: A color scheme based directly on the main color selected, using different shades of the main color and mixing in greys to create a color scheme. The background of monochrome light would be the foreground or text color on Monochrome Dark and vice versa. \n Analogic: A color scheme based on the main color selected, using the colors adjacent to the main color on the color wheel. \n Analogic Complementary: A color scheme based on the main color selected, using the colors adjacent to the main color on the color wheel and the complementary color. \n Triad: A color scheme based on the main color selected, using the colors on the color wheel that are equidistant from the main color. \n Quad: A color scheme based on the main color selected, using the colors on the color wheel that are 90 degrees from the main color."
},
"devTools": {
"colorIniEditorPlaceholder": "[your-color-scheme-name]"
Expand Down
5 changes: 4 additions & 1 deletion src/resources/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
"hideInstalledLabel": "Hide installed when browsing",
"colourShiftLabel": "Shift colours every minute",
"albumArtBasedColors": "Change colours based on album art",
"albumArtBasedColorsMode": "The mode that the colorapi uses to generate colour schemes",
"albumArtBasedColorsMode": "Colour scheme (ColorApi) mode",
"albumArtBasedColorsVibrancy": "Colour grabbed from album art",
"albumArtBasedColorsVibrancyToolTip": "Desaturated: The colour that is the most prominent but with much less brightness \n Light Vibrant: The most Vibrant colour but with the brightness amped up a tad \n Prominent: The colour that pops the most in the album art \n Vibrant: The most vibrant colour in the album art",
"almbumArtColorsModeToolTip": "Monochrome Dark: A colour scheme based directly on the main colour selected, using different shades of the main colour and mixing in greys to create a colour scheme, this is the inverse of Monochrome Light. \n Monochrome Light: A colour scheme based directly on the main colour selected, using different shades of the main colour and mixing in greys to create a colour scheme. The background of monochrome light would be the foreground or text colour on Monochrome Dark and vice versa. \n Analogic: A colour scheme based on the main colour selected, using the colours adjacent to the main colour on the colour wheel. \n Analogic Complementary: A colour scheme based on the main colour selected, using the colours adjacent to the main colour on the colour wheel and the complementary colour. \n Triad: A colour scheme based on the main colour selected, using the colours on the colour wheel that are equidistant from the main colour. \n Quad: A colour scheme based on the main colour selected, using the colours on the colour wheel that are 90 degrees from the main colour.",
"tabsHeading": "Tabs",
"resetHeading": "Reset",
"resetBtn": "$t(settings.resetHeading)",
Expand Down
19 changes: 19 additions & 0 deletions src/styles/components/_settings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,29 @@
&.action {
// float: right;
text-align: right;

// The tooltip icon
& .marketplace-sortBox + .marketplace-tooltip-icon {
margin-inline-start: 8px;
}
}
}
}

// The tooltip icon
.marketplace-tooltip-icon {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;

// The tooltips themselves
& + [data-tippy-root] {
text-align: start;
}
}

// The up/down arrows
button.arrow-btn {
align-items: center;
Expand Down
1 change: 1 addition & 0 deletions src/types/marketplace-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export type VisualConfig = {
themeDevTools: boolean;
albumArtBasedColors: boolean;
albumArtBasedColorsMode: "monochromeLight" | "monochromeDark" | "quad" | "triad" | "analogic" | "analogicComplement";
albumArtBasedColorsVibrancy: "DESATURATED" | "LIGHT_VIBRANT" | "PROMINENT" | "VIBRANT";
// Legacy from reddit app
type: boolean;
// I was considering adding watchers as "followers" but it looks like the value is a duplicate
Expand Down
Loading