mirror of
https://github.com/danbulant/dribbblish-dynamic-theme
synced 2026-05-24 12:35:05 +00:00
commit
f6aa2a5c7d
6 changed files with 141 additions and 66 deletions
|
|
@ -1,5 +1,5 @@
|
|||
Added:
|
||||
- `Report Bugs` and `Changelog` buttons to `Settings > About`
|
||||
- `Report Bugs`, `Request Feature` and `Changelog` buttons to `Settings > About`
|
||||
- Markdown parsing for settings descriptions
|
||||
- Option to have a button to open the settings next to your profile picture
|
||||
|
||||
|
|
@ -17,4 +17,5 @@ Improved:
|
|||
- Checkbox / Switch input styles are now more in line with other input styles
|
||||
- Available updates are now shown as a clickable button next to your user icon instead of having to open the user menu
|
||||
- The "offline" icon is now handled by dribbblish and fits in with the other info icons
|
||||
- Hovering over the release date in the album info now shows the full date
|
||||
- Hovering over the release date in the album info now shows the full date
|
||||
- Better color extraction from the cover image
|
||||
|
|
@ -18,11 +18,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"chroma-js": "^2.1.2",
|
||||
"colorthief": "^2.3.2",
|
||||
"jquery": "^3.6.0",
|
||||
"markdown-it": "^12.2.0",
|
||||
"markdown-it-attrs": "^4.1.0",
|
||||
"markdown-it-bracketed-spans": "^1.0.1",
|
||||
"moment": "^2.29.1",
|
||||
"node-vibrant": "3.1.4"
|
||||
"node-vibrant": "^3.1.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import $ from "jquery";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import MarkdownItAttrs from "markdown-it-attrs";
|
||||
import markdownItBracketedSpans from "markdown-it-bracketed-spans";
|
||||
import MarkdownItBracketedSpans from "markdown-it-bracketed-spans";
|
||||
|
||||
import svgUndo from "svg/undo";
|
||||
|
||||
|
|
@ -82,8 +82,8 @@ export default class ConfigMenu {
|
|||
linkify: true,
|
||||
typographer: true
|
||||
});
|
||||
this.#md.use(MarkdownItBracketedSpans);
|
||||
this.#md.use(MarkdownItAttrs);
|
||||
this.#md.use(markdownItBracketedSpans);
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.id = "dribbblish-config";
|
||||
|
|
|
|||
|
|
@ -33,3 +33,7 @@ export function copyToClipboard(text) {
|
|||
export function capitalizeFirstLetter(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
export function getClosestToNum(arr, num) {
|
||||
return arr.reduce((prev, curr) => (Math.abs(curr - num) < Math.abs(prev - num) ? curr : prev));
|
||||
}
|
||||
|
|
|
|||
189
src/js/main.js
189
src/js/main.js
|
|
@ -1,9 +1,10 @@
|
|||
import * as Vibrant from "node-vibrant";
|
||||
import ColorThief from "colorthief";
|
||||
import chroma from "chroma-js";
|
||||
import $ from "jquery";
|
||||
import moment from "moment";
|
||||
|
||||
import { waitForElement, copyToClipboard, capitalizeFirstLetter } from "./Util";
|
||||
import { waitForElement, copyToClipboard, capitalizeFirstLetter, getClosestToNum } from "./Util";
|
||||
import ConfigMenu from "./ConfigMenu";
|
||||
import Info from "./Info";
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ const Dribbblish = {
|
|||
config: new ConfigMenu(),
|
||||
info: new Info()
|
||||
};
|
||||
const colorThief = new ColorThief();
|
||||
// To expose to external scripts
|
||||
window.Dribbblish = Dribbblish;
|
||||
|
||||
|
|
@ -387,7 +389,7 @@ waitForElement(["#main"], () => {
|
|||
type: "button",
|
||||
key: "aboutDribbblishBugs",
|
||||
name: "Report Bugs",
|
||||
description: "Open new issue on GitHub",
|
||||
description: "Open new issue on GitHub to report a bug",
|
||||
data: "Create Report",
|
||||
onChange: () => {
|
||||
const reportBody = `
|
||||
|
|
@ -442,6 +444,22 @@ waitForElement(["#main"], () => {
|
|||
}
|
||||
});
|
||||
|
||||
Dribbblish.config.register({
|
||||
area: "About",
|
||||
type: "button",
|
||||
key: "aboutDribbblishFeature",
|
||||
name: "Request Feature",
|
||||
description: "Open new issue on GitHub to request a feature",
|
||||
data: "Request Feature",
|
||||
onChange: () => {
|
||||
const reportURL = new URL("https://github.com/JulienMaille/dribbblish-dynamic-theme/issues/new");
|
||||
reportURL.searchParams.set("labels", "enhancement");
|
||||
reportURL.searchParams.set("template", "feature_request.md");
|
||||
|
||||
window.open(reportURL.toString(), "_blank");
|
||||
}
|
||||
});
|
||||
|
||||
Dribbblish.config.register({
|
||||
area: "About",
|
||||
type: "button",
|
||||
|
|
@ -460,9 +478,7 @@ async function getAlbumRelease(uri) {
|
|||
}
|
||||
|
||||
function isLight(hex) {
|
||||
var [r, g, b] = chroma(hex).rgb().map(Number);
|
||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
return brightness > 128;
|
||||
return chroma(hex).luminance() > 0.5;
|
||||
}
|
||||
|
||||
// From: https://stackoverflow.com/a/13763063/12126879
|
||||
|
|
@ -492,10 +508,8 @@ function getImageLightness(img) {
|
|||
return brightness;
|
||||
}
|
||||
|
||||
// parse to hex beacuse "--spice-sidebar" is `rgb()`
|
||||
let textColor = chroma($("html").css("--spice-text")).hex();
|
||||
// parse to hex because "--spice-sidebar" is `rgb()`
|
||||
let textColorBg = chroma($("html").css("--spice-main")).hex();
|
||||
let sidebarColor = chroma($("html").css("--spice-sidebar")).hex();
|
||||
|
||||
function setRootColor(name, color) {
|
||||
$("html").css(`--spice-${name}`, chroma(color).hex());
|
||||
|
|
@ -514,10 +528,10 @@ function toggleDark(setDark) {
|
|||
setRootColor("subtext", setDark ? "#EAEAEA" : "#3D3D3D");
|
||||
setRootColor("notification", setDark ? "#303030" : "#DDDDDD");
|
||||
|
||||
updateColors(textColor, sidebarColor, false);
|
||||
updateColors(false);
|
||||
}
|
||||
|
||||
function checkDarkLightMode(colors) {
|
||||
function checkDarkLightMode() {
|
||||
const theme = Dribbblish.config.get("theme");
|
||||
if (theme == "time") {
|
||||
const start = 60 * parseInt(Dribbblish.config.get("darkModeOnTime").split(":")[0]) + parseInt(Dribbblish.config.get("darkModeOnTime").split(":")[1]);
|
||||
|
|
@ -530,22 +544,31 @@ function checkDarkLightMode(colors) {
|
|||
if (end < start) dark = start <= time || time < end;
|
||||
else dark = start <= time && time < end;
|
||||
toggleDark(dark);
|
||||
} else if (theme == "color") {
|
||||
if (colors && colors.length > 0) toggleDark(isLight(colors[0]));
|
||||
}
|
||||
}
|
||||
|
||||
// Run every Minute to check time and set dark / light mode
|
||||
setInterval(checkDarkLightMode, 60000);
|
||||
|
||||
Dribbblish.config.register({
|
||||
area: "Theme",
|
||||
type: "checkbox",
|
||||
key: "dynamicColors",
|
||||
name: "Dynamic",
|
||||
description: "If the Theme's Color should be extracted from Albumart",
|
||||
defaultValue: true,
|
||||
onChange: (val) => updateColors(),
|
||||
showChildren: (val) => !val,
|
||||
type: "select",
|
||||
key: "colorSelectionAlgorithm",
|
||||
name: "Color Selection Algorithm",
|
||||
description: `
|
||||
[Algorithm of selecting colors from the albumart]
|
||||
- **Colorthief [(see)](https://lokeshdhakar.com/projects/color-thief/):** Gets more fitting colors
|
||||
- **Vibrant [(see)](https://jariz.github.io/vibrant.js/):** Gets more vibrant colors *(was the default up to v3.1.1)*
|
||||
- **Static:** Select a static color to be used
|
||||
{.muted}
|
||||
`,
|
||||
data: { colorthief: "Colorthief", vibrant: "Vibrant", static: "Static" },
|
||||
defaultValue: "colorthief",
|
||||
onChange: () => updateColors(),
|
||||
showChildren: (val) => {
|
||||
if (val == "static") return ["colorOverride"];
|
||||
return ["colorSelectionMode"];
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "color",
|
||||
|
|
@ -554,7 +577,53 @@ Dribbblish.config.register({
|
|||
description: "The Color of the Theme",
|
||||
defaultValue: "#1ed760",
|
||||
fireInitialChange: false,
|
||||
onChange: (val) => updateColors()
|
||||
onChange: () => updateColors()
|
||||
},
|
||||
{
|
||||
area: "Theme",
|
||||
type: "select",
|
||||
key: "colorSelectionMode",
|
||||
name: "Color Selection Mode",
|
||||
description: `
|
||||
Method of selecting colors from the albumart
|
||||
- **Default:** Choose closest matching{.muted}
|
||||
- **Luminance:** Choose matching current theme (lighter/darker){.muted}
|
||||
`,
|
||||
data: { default: "Default", luminance: "Luminance" },
|
||||
defaultValue: "default",
|
||||
onChange: () => updateColors(),
|
||||
showChildren: (val) => {
|
||||
if (val == "dynamicLuminance") return ["lightModeLuminance", "darkModeLuminance"];
|
||||
return false;
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: "number",
|
||||
key: "lightModeLuminance",
|
||||
name: "Desired Light Mode Luminance",
|
||||
description: `
|
||||
Set desired luminance in light mode.
|
||||
*the selected color will be the one who's luminance is closest to the desired luminance*{.muted}
|
||||
`,
|
||||
defaultValue: 0.6,
|
||||
data: { min: 0, max: 1, step: 0.05 },
|
||||
fireInitialChange: false,
|
||||
onChange: () => updateColors()
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
key: "darkModeLuminance",
|
||||
name: "Desired Dark Mode Luminance",
|
||||
description: `
|
||||
Set desired luminance in dark mode.
|
||||
*the selected color will be the one who's luminance is closest to the desired luminance*{.muted}
|
||||
`,
|
||||
defaultValue: 0.2,
|
||||
data: { min: 0, max: 1, step: 0.05 },
|
||||
fireInitialChange: false,
|
||||
onChange: () => updateColors()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
@ -562,7 +631,7 @@ Dribbblish.config.register({
|
|||
Dribbblish.config.register({
|
||||
area: "Theme",
|
||||
type: "select",
|
||||
data: { dark: "Dark", light: "Light", time: "Based on Time", color: "Based on Color" },
|
||||
data: { dark: "Dark", light: "Light", time: "Based on Time" },
|
||||
key: "theme",
|
||||
name: "Theme",
|
||||
description: "Select Dark / Bright mode",
|
||||
|
|
@ -582,9 +651,6 @@ Dribbblish.config.register({
|
|||
case "time":
|
||||
checkDarkLightMode();
|
||||
break;
|
||||
case "color":
|
||||
checkDarkLightMode();
|
||||
break;
|
||||
}
|
||||
},
|
||||
children: [
|
||||
|
|
@ -609,27 +675,16 @@ Dribbblish.config.register({
|
|||
]
|
||||
});
|
||||
|
||||
var currentColor;
|
||||
var currentSideColor;
|
||||
|
||||
function updateColors(textColHex, sideColHex, checkDarkMode = true) {
|
||||
if (textColHex && sideColHex) {
|
||||
currentColor = textColHex;
|
||||
currentSideColor = sideColHex;
|
||||
} else {
|
||||
if (!(currentColor && currentSideColor)) return; // If `updateColors()` is called early these vars are undefined and would break
|
||||
textColHex = currentColor;
|
||||
sideColHex = currentSideColor;
|
||||
}
|
||||
|
||||
if (!Dribbblish.config.get("dynamicColors")) {
|
||||
const col = Dribbblish.config.get("colorOverride");
|
||||
textColHex = col;
|
||||
sideColHex = col;
|
||||
}
|
||||
function updateColors(checkDarkMode = true, sideColHex) {
|
||||
if (sideColHex == undefined) return registerCoverListener();
|
||||
|
||||
let isLightBg = isLight(textColorBg);
|
||||
if (isLightBg) textColHex = chroma(textColHex).darken(0.15).hex(); // vibrant color is always too bright for white bg mode
|
||||
let textColHex = sideColHex;
|
||||
if (isLightBg && chroma(textColHex).luminance() > 0.2) {
|
||||
textColHex = chroma(textColHex).luminance(0.2).hex();
|
||||
} else if (!isLightBg && chroma(textColHex).luminance() < 0.1) {
|
||||
textColHex = chroma(textColHex).luminance(0.1).hex();
|
||||
}
|
||||
|
||||
let darkColHex = chroma(textColHex)
|
||||
.brighten(isLightBg ? 0.12 : -0.2)
|
||||
|
|
@ -675,8 +730,6 @@ async function songchange() {
|
|||
let bgImage = Spicetify.Player.data.track.metadata.image_url;
|
||||
if (bgImage === undefined) {
|
||||
bgImage = "/images/tracklist-row-song-fallback.svg";
|
||||
textColor = "#509bf5";
|
||||
updateColors(textColor, textColor);
|
||||
}
|
||||
|
||||
if (album_uri !== undefined && !album_uri.includes("spotify:show")) {
|
||||
|
|
@ -719,25 +772,41 @@ async function pickCoverColor(img) {
|
|||
|
||||
$("html").css("--image-brightness", getImageLightness(img) / 255);
|
||||
|
||||
var swatches = await new Promise((resolve, reject) => new Vibrant(img, 5).getPalette().then(resolve).catch(reject));
|
||||
var lightCols = ["Vibrant", "DarkVibrant", "Muted", "LightVibrant"];
|
||||
var darkCols = ["Vibrant", "LightVibrant", "Muted", "DarkVibrant"];
|
||||
let color = "#509bf5";
|
||||
if (img.complete) {
|
||||
const colorSelectionAlgorithm = Dribbblish.config.get("colorSelectionAlgorithm");
|
||||
const colorSelectionMode = Dribbblish.config.get("colorSelectionMode");
|
||||
let palette = {};
|
||||
|
||||
var mainCols = isLight(textColorBg) ? lightCols : darkCols;
|
||||
textColor = "#509bf5";
|
||||
for (var col in mainCols)
|
||||
if (swatches[mainCols[col]]) {
|
||||
textColor = swatches[mainCols[col]].getHex();
|
||||
break;
|
||||
if (colorSelectionAlgorithm == "colorthief") {
|
||||
palette = Object.fromEntries([colorThief.getColor(img), ...colorThief.getPalette(img, 24, 5)].map((c) => chroma(c)).map((c) => [c.luminance(), c]));
|
||||
} else if (colorSelectionAlgorithm == "vibrant") {
|
||||
const swatches = await new Promise((resolve, reject) => new Vibrant(img, 5).getPalette().then(resolve).catch(reject));
|
||||
for (var col of ["Vibrant", "DarkVibrant", "Muted", "LightVibrant"]) {
|
||||
if (swatches[col]) {
|
||||
const c = chroma(swatches[col].getHex());
|
||||
palette[c.luminance()] = c;
|
||||
}
|
||||
}
|
||||
} else if (colorSelectionAlgorithm == "static") {
|
||||
palette[1] = chroma(Dribbblish.config.get("colorOverride"));
|
||||
}
|
||||
|
||||
sidebarColor = "#509bf5";
|
||||
for (var col in lightCols)
|
||||
if (swatches[lightCols[col]]) {
|
||||
sidebarColor = swatches[lightCols[col]].getHex();
|
||||
break;
|
||||
if (colorSelectionMode == "default") {
|
||||
color = Object.values(palette)[0];
|
||||
for (const col of Object.values(palette)) {
|
||||
if (col.luminance() > 0.05 && col.luminance() < 0.9) {
|
||||
color = col.hex();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (colorSelectionMode == "luminance") {
|
||||
const wantedLuminance = $("html").css("--is_light") == "1" ? Dribbblish.config.get("lightModeLuminance") : Dribbblish.config.get("darkModeLuminance");
|
||||
color = palette[getClosestToNum(Object.keys(palette), wantedLuminance)].hex();
|
||||
}
|
||||
updateColors(textColor, sidebarColor);
|
||||
}
|
||||
|
||||
updateColors(false, color);
|
||||
}
|
||||
|
||||
var coverListener;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@
|
|||
}
|
||||
|
||||
.muted {
|
||||
opacity: 0.5;
|
||||
color: spiceColor("subtext", 0.5);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue