diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12e531a..518ea0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,18 @@
Added:
- `Report Bugs` 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
Fixed:
- Fonts looking blurry
- Notification popups are being invisible when the (dribbblish) settings are open
- Missing on/off times settings for `Based on Time` dark mode (#107)
- Playing icon position being wrong when listening to a playlist that is inside a folder ([#106 (comment)](https://github.com/JulienMaille/dribbblish-dynamic-theme/issues/106#issuecomment-967208507))
+- Alignment of right expanded cover
+- Slider tooltip is incorrect after a reset (#111)
Improved:
+- The settings UI now better represents grouped items
- Settings that have been changed from default will now show a line next to them. Inspired by the [Visual Studio Code settings UI](https://d33wubrfki0l68.cloudfront.net/d1f1ea4def506997ced23d3d912154794e530e1c/063d2/assets/img/blog/2020-09-17-vscode-settings/settings-ui.png)
- 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
diff --git a/package.json b/package.json
index 6313c79..a2782f0 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"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.6"
}
diff --git a/src/js/ConfigMenu.js b/src/js/ConfigMenu.js
index d623bae..f3e5ebb 100644
--- a/src/js/ConfigMenu.js
+++ b/src/js/ConfigMenu.js
@@ -1,6 +1,7 @@
import $ from "jquery";
import MarkdownIt from "markdown-it";
import MarkdownItAttrs from "markdown-it-attrs";
+import markdownItBracketedSpans from "markdown-it-bracketed-spans";
import svgUndo from "svg/undo";
@@ -11,7 +12,7 @@ export default class ConfigMenu {
* @property {String|DribbblishConfigArea} [area={name: "Main Settings", order: 0}]
* @property {any} [data={}]
* @property {Number} [order=0] order < 0 = Higher up | order > 0 = Lower Down
- * @property {String} [key] defaults to `${area}_${name]`. e.g: About_Info
+ * @property {String} key
* @property {String} name
* @property {String} [description=""]
* @property {any} [defaultValue]
@@ -20,6 +21,7 @@ export default class ConfigMenu {
* @property {Boolean} [insertOnTop=false]
* @property {Boolean} [fireInitialChange=true]
* @property {Boolean} [save=true]
+ * @property {validate} [validate]
* @property {showChildren} [showChildren]
* @property {onAppended} [onAppended]
* @property {onChange} [onChange]
@@ -34,6 +36,13 @@ export default class ConfigMenu {
* @property {Boolean} [toggleable=true]
*/
+ /**
+ * @callback validate
+ * @this {DribbblishConfigItem}
+ * @param {any} value
+ * @returns {Boolean | String}
+ */
+
/**
* @callback showChildren
* @this {DribbblishConfigItem}
@@ -74,6 +83,7 @@ export default class ConfigMenu {
typographer: true
});
this.#md.use(MarkdownItAttrs);
+ this.#md.use(markdownItBracketedSpans);
const container = document.createElement("div");
container.id = "dribbblish-config";
@@ -116,6 +126,7 @@ export default class ConfigMenu {
elem.setAttribute("type", options.type);
if (options.hidden) elem.setAttribute("hidden", true);
if (options.childOf) elem.setAttribute("parent", options.childOf);
+ if (options.children.length > 0) elem.setAttribute("children", options.children.map((c) => c.key).join(" "));
elem.innerHTML = /* html */ `
${
options.name != null && options.description != null
@@ -154,6 +165,7 @@ export default class ConfigMenu {
$inputElem.prop("checked", defaultVal);
} else {
$inputElem.prop("value", defaultVal);
+ if (options.type == "slider") $inputElem.attr("tooltip", defaultVal);
}
options.onChange(defaultVal);
});
@@ -177,6 +189,7 @@ export default class ConfigMenu {
insertOnTop: false,
fireInitialChange: true,
save: true,
+ validate: () => true,
showChildren: () => true,
onAppended: () => {},
onChange: () => {},
@@ -186,7 +199,6 @@ export default class ConfigMenu {
// Set Defaults
options = { ...defaultOptions, ...options };
if (typeof options.area == "string") options.area = { name: options.area, order: 0 };
- if (options.key == null) options.key = `${options.area.name}_${options.name}`.split(" ").join("_");
options.description = options.description
.split("\n")
.filter((line) => line.trim() != "")
@@ -194,7 +206,11 @@ export default class ConfigMenu {
.join("\n");
options._onChange = options.onChange;
options.onChange = (val) => {
- $(`.dribbblish-config-item[key="${options.key}"]`).attr("changed", options.save && val != options.defaultValue ? "" : null);
+ const isValid = validate(val);
+ $(`.dribbblish-config-item[key="${options.key}"]`).attr("changed", isValid === true && val != options.defaultValue ? "" : null);
+ if (!isValid) return;
+ this.set(options.key, val, options.save);
+
options._onChange.call(options, val);
const show = options.showChildren.call(options, val);
options.children.forEach((child) => this.#setHidden(child.key, Array.isArray(show) ? !show.includes(child.key) : !show));
@@ -205,6 +221,18 @@ export default class ConfigMenu {
this.#config[options.key] = options;
+ function validate(val) {
+ const isValid = options.validate.call(options, val);
+ const $elem = $(`.dribbblish-config-item[key="${options.key}"]`);
+ if (isValid === true) {
+ $elem.attr("invalid", null).css("--validation-error", "");
+ } else {
+ const error = isValid === false ? "Invalid" : isValid;
+ $elem.attr("invalid", "").css("--validation-error", `"${error.replace(/"/g, `\\"`)}"`);
+ }
+ return isValid;
+ }
+
if (options.type == "checkbox") {
const input = /* html */ `
@@ -215,8 +243,7 @@ export default class ConfigMenu {
this.#addInputHTML({ ...options, input });
$(`#dribbblish-config-input-${options.key}`).on("change", (e) => {
- this.set(options.key, e.target.checked, options.save);
- options.onChange(this.get(options.key));
+ options.onChange(e.target.checked);
});
} else if (options.type == "select") {
// Validate
@@ -233,8 +260,7 @@ export default class ConfigMenu {
this.#addInputHTML({ ...options, input });
$(`#dribbblish-config-input-${options.key}`).on("change", (e) => {
- this.set(options.key, e.target.value, options.save);
- options.onChange(this.get(options.key));
+ options.onChange(e.target.value);
});
} else if (options.type == "button") {
if (typeof options.data != "string") options.data = options.name;
@@ -257,9 +283,9 @@ export default class ConfigMenu {
} else if (options.type == "number") {
// Validate
if (options.defaultValue == null) options.defaultValue = 0;
- const val = this.get(options.key);
- if (options.data.min != null && val < options.data.min) this.set(options.key, options.data.min, options.save);
- if (options.data.max != null && val > options.data.max) this.set(options.key, options.data.max, options.save);
+ const _val = this.get(options.key);
+ if (options.data.min != null && _val < options.data.min) this.set(options.key, options.data.min, options.save);
+ if (options.data.max != null && _val > options.data.max) this.set(options.key, options.data.max, options.save);
const input = /* html */ `
options.data.max) e.target.value = options.data.max;
- this.set(options.key, Number(e.target.value), options.save);
- options.onChange(this.get(options.key));
+ options.onChange(Number(e.target.value));
});
} else if (options.type == "text") {
if (options.defaultValue == null) options.defaultValue = "";
@@ -294,9 +319,10 @@ export default class ConfigMenu {
this.#addInputHTML({ ...options, input });
$(`#dribbblish-config-input-${options.key}`).on("input", (e) => {
- // TODO: maybe add an validation function via `data.validate`
- this.set(options.key, e.target.value, options.save);
- options.onChange(this.get(options.key));
+ const val = e.target.value;
+ if (!validate(val)) return;
+ this.set(options.key, val, options.save);
+ options.onChange(val);
});
} else if (options.type == "textarea") {
if (options.defaultValue == null) options.defaultValue = "";
@@ -307,9 +333,7 @@ export default class ConfigMenu {
this.#addInputHTML({ ...options, input });
$(`#dribbblish-config-input-${options.key}`).on("input", (e) => {
- // TODO: maybe add an validation function via `data.validate`
- this.set(options.key, e.target.value, options.save);
- options.onChange(this.get(options.key));
+ options.onChange(e.target.value);
});
} else if (options.type == "slider") {
// Validate
@@ -336,8 +360,7 @@ export default class ConfigMenu {
$(`#dribbblish-config-input-${options.key}`).attr("tooltip", `${e.target.value}${options.data?.suffix ?? ""}`);
$(`#dribbblish-config-input-${options.key}`).attr("value", e.target.value);
- this.set(options.key, Number(e.target.value), options.save);
- options.onChange(this.get(options.key));
+ options.onChange(Number(e.target.value));
});
} else if (options.type == "time") {
// Validate
@@ -350,8 +373,7 @@ export default class ConfigMenu {
$(`#dribbblish-config-input-${options.key}`).on("input", (e) => {
$(`#dribbblish-config-input-${options.key}`).attr("value", e.target.value);
- this.set(options.key, e.target.value, options.save);
- options.onChange(this.get(options.key));
+ options.onChange(e.target.value);
});
} else if (options.type == "color") {
// Validate
@@ -362,8 +384,7 @@ export default class ConfigMenu {
this.#addInputHTML({ ...options, input });
$(`#dribbblish-config-input-${options.key}`).on("input", (e) => {
- this.set(options.key, e.target.value, options.save);
- options.onChange(this.get(options.key));
+ options.onChange(e.target.value);
});
} else {
throw new Error(`Config Type "${options.type}" invalid`);
@@ -372,6 +393,7 @@ export default class ConfigMenu {
// Re-write internal config since some values may have changed
this.#config[options.key] = options;
+ validate(this.get(options.key));
$(`.dribbblish-config-item[key="${options.key}"]`).attr("changed", options.save && this.get(options.key) != options.defaultValue ? "" : null);
options.children.forEach((child) => this.register(child));
@@ -466,7 +488,32 @@ export default class ConfigMenu {
*/
#setHidden(key, hidden) {
this.#config[key].hidden = hidden;
- $(`.dribbblish-config-item[key="${key}"]`).attr("hidden", hidden ? "" : null);
+ const $elem = $(`.dribbblish-config-item[key="${key}"]`);
+ $elem.attr("hidden", hidden ? "" : null);
+
+ // If element has children or a parent
+ if ($elem.attr("parent") != null || $elem.attr("children") != null) {
+ // Get parent of element block
+ const $parent = $elem.attr("parent") != null ? $(`[children~="${key}"]`) : $elem;
+ const $nextChildren = $parent.nextAll(`[parent="${$parent.attr("key")}"]`);
+
+ // Make parent connect on bottom when children are visible
+ $parent.attr("connect-bottom", $nextChildren.filter(":not([hidden])").length > 0 ? "" : null);
+
+ // Reset all children's bottom connection
+ $nextChildren.each(function () {
+ $(this).attr("connect-bottom", null);
+ });
+ // Add bottom connection to all but the last visible child
+ $nextChildren
+ .filter(":not([hidden])")
+ .slice(0, -1)
+ .each(function () {
+ $(this).attr("connect-bottom", "");
+ });
+
+ //* NOTE: All children automatically have a top connection
+ }
}
getOptions(key) {
diff --git a/src/js/Info.js b/src/js/Info.js
index 80ba54a..7d4f99c 100644
--- a/src/js/Info.js
+++ b/src/js/Info.js
@@ -8,11 +8,17 @@ export default class Info {
* @property {String} [text]
* @property {String} [tooltip]
* @property {String} [icon]
- * @property {{fg: String, bg: String}} [color] defaults to {fg: "sidebar-text", bg: "button"}
+ * @property {DribbblishInfoColor} [color]
* @property {Number} [order=0] order < 0 = More to the Left | order > 0 = More to the Right
* @property {onClick} [onClick]
*/
+ /**
+ * @typedef {Object} DribbblishInfoColor
+ * @property {String} [fg]
+ * @property {String} [bg]
+ */
+
/**
* @callback onClick
* @returns {void}
@@ -58,9 +64,9 @@ export default class Info {
if (info.tooltip != null) elem.setAttribute("title", info.tooltip);
if (info.onClick != null) elem.setAttribute("clickable", "");
if (info.color != null) {
- const { bg, fg } = info.color;
- if (bg != null) elem.style.backgroundColor = bg;
+ const { fg, bg } = info.color;
if (fg != null) elem.style.color = fg;
+ if (bg != null) elem.style.backgroundColor = bg;
}
if (info.order != 0) elem.style.order = info.order;
elem.innerHTML = `${info.text ?? ""}${info.icon ?? ""}`;
diff --git a/src/js/main.js b/src/js/main.js
index 42f0881..a794005 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -11,6 +11,7 @@ import Info from "./Info";
import svgArrowDown from "svg/arrow-down";
import svgCode from "svg/code";
import svgWifiSlash from "svg/wifi-slash";
+import svgCog from "svg/cog";
const Dribbblish = {
config: new ConfigMenu(),
@@ -20,6 +21,25 @@ const colorThief = new ColorThief();
// To expose to external scripts
window.Dribbblish = Dribbblish;
+Dribbblish.config.register({
+ type: "checkbox",
+ key: "openSettingsInfo",
+ name: "Open Settings Icon",
+ description: "Show an icon next to your profile image to open the dribbblish settings",
+ defaultValue: true,
+ onChange: (val) =>
+ Dribbblish.info[val ? "set" : "remove"]("settings", {
+ icon: svgCog,
+ color: {
+ fg: "var(--spice-subtext)",
+ bg: "rgba(var(--spice-rgb-subtext), calc(0.1 + var(--is_light) * 0.05))"
+ },
+ order: 999,
+ tooltip: "Open Dribbblish Settings",
+ onClick: () => Dribbblish.config.open()
+ })
+});
+
Dribbblish.config.register({
type: "checkbox",
key: "rightBigCover",
@@ -598,7 +618,7 @@ Dribbblish.config.register({
key: "theme",
name: "Theme",
description: "Select Dark / Bright mode",
- defaultValue: "dark",
+ defaultValue: "time",
showChildren: (val) => {
if (val == "time") return ["darkModeOnTime", "darkModeOffTime"];
return false;
@@ -803,7 +823,7 @@ waitForElement([".main-topBar-container"], ([topBarContainer]) => {
.then((response) => response.json())
.then((data) => {
const isDev = process.env.DRIBBBLISH_VERSION == "Dev";
- Dribbblish.info.set("upd", isDev || data.tag_name > process.env.DRIBBBLISH_VERSION ? { text: `v${data.tag_name}`, tooltip: "Open Release page to download", icon: svgArrowDown, onClick: () => window.open("https://github.com/JulienMaille/dribbblish-dynamic-theme/releases/latest", "_blank") } : null);
+ Dribbblish.info.set("update", isDev || data.tag_name > process.env.DRIBBBLISH_VERSION ? { text: `v${data.tag_name}`, tooltip: "Open Release page to download", icon: svgArrowDown, onClick: () => window.open("https://github.com/JulienMaille/dribbblish-dynamic-theme/releases/latest", "_blank") } : null);
Dribbblish.info.set("dev", isDev ? { tooltip: "Dev build", icon: svgCode } : null);
})
.catch(console.error);
@@ -818,7 +838,7 @@ window.addEventListener("offline", () =>
Dribbblish.info.set("offline", {
tooltip: "Offline",
icon: svgWifiSlash,
- order: 999,
+ order: 998,
color: {
fg: "#ffffff",
bg: "#ff2323"
diff --git a/src/styles/Colors.scss b/src/styles/Colors.scss
index 4be09b6..138f53e 100644
--- a/src/styles/Colors.scss
+++ b/src/styles/Colors.scss
@@ -46,10 +46,13 @@ $props-to-transition: ("sidebar", "main", "text", "button");
}
// Color Function
-@function spiceColor($key, $alpha: 1) {
+// $light-offset is added when in light mode
+@function spiceColor($key, $alpha: 1, $light-offset: 0) {
@if $alpha == 1 {
@return var(--spice-#{$key});
- } @else {
+ } @else if $light-offset == 0 {
@return rgba(var(--spice-rgb-#{$key}), $alpha);
+ } @else {
+ @return rgba(var(--spice-rgb-#{$key}), calc($alpha + var(--is_light) * $light-offset));
}
}
diff --git a/src/styles/ConfigMenu.scss b/src/styles/ConfigMenu.scss
index c0a90a2..ede6c99 100644
--- a/src/styles/ConfigMenu.scss
+++ b/src/styles/ConfigMenu.scss
@@ -23,7 +23,7 @@
z-index: 1;
position: relative;
width: clamp(500px, 50%, 650px);
- background-color: spiceColor("main", 0.9);
+ background-color: spiceColor("main", 0.95);
backdrop-filter: blur(3px);
padding: 20px 15px;
border-radius: var(--main-corner-radius);
@@ -44,22 +44,23 @@
display: flex;
width: 100%;
flex-direction: column;
- gap: 16px;
+ gap: 8px;
max-height: 60vh;
overflow-y: auto;
- padding: 0px 50px;
+ padding: 0px 25px;
.dribbblish-config-area {
display: flex;
flex-direction: column;
- gap: 16px;
+ align-items: center;
+ gap: 8px;
&[collapsed] {
overflow: hidden;
min-height: 38px; //for some reason height alone isn't enough
height: 38px;
- > h2 svg {
+ .dribbblish-config-area-header svg {
transform: rotate(270deg);
}
}
@@ -71,6 +72,7 @@
.dribbblish-config-area-header {
position: relative;
text-align: center;
+ width: fit-content;
height: 38px;
display: flex;
gap: 10px;
@@ -89,7 +91,8 @@
.dribbblish-config-area-items {
display: flex;
flex-direction: column;
- gap: 16px;
+ gap: 8px;
+ width: 100%;
.dribbblish-config-item {
position: relative;
@@ -99,23 +102,58 @@
justify-content: space-between;
align-items: center;
gap: 10px;
+ padding: 8px 16px;
&[hidden] {
display: none;
}
- &[parent] {
- padding-left: 16px;
- }
-
- &[changed]::before {
+ &::before {
+ z-index: -1;
content: "";
position: absolute;
- left: -13px;
- top: -3px;
- bottom: -3px;
- width: 3px;
- background-color: spiceColor("sidebar");
+ inset: 0px;
+ border-radius: var(--main-corner-radius);
+ background-color: spiceColor("subtext", 0.03, 0.04);
+ }
+
+ &[parent]::before {
+ top: -8px;
+ border-top-left-radius: 0px;
+ border-top-right-radius: 0px;
+ }
+
+ &[connect-bottom]::before {
+ border-bottom-left-radius: 0px;
+ border-bottom-right-radius: 0px;
+ }
+
+ &[invalid]::before {
+ border: 2px solid rgba(red, 0.8);
+ }
+
+ &[changed] {
+ &::after {
+ content: "";
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ bottom: 0px;
+ width: 5px;
+ background-color: spiceColor("text");
+ border-top-left-radius: var(--main-corner-radius);
+ border-bottom-left-radius: var(--main-corner-radius);
+ }
+
+ &[parent]::after {
+ top: -4px;
+ border-top-left-radius: 0px;
+ }
+
+ &[connect-bottom]::after {
+ bottom: -4px;
+ border-bottom-left-radius: 0px;
+ }
}
.dribbblish-config-item-header {
@@ -152,6 +190,7 @@
height: min-content;
color: spiceColor("subtext");
line-height: calc(1em + 6px); // To have line gaps
+ line-break: anywhere;
}
.x-settings-secondColumn {
@@ -162,6 +201,12 @@
.dribbblish-config-item-input {
min-width: fit-content;
+
+ &::before {
+ content: var(--validation-error);
+ margin-right: 8px;
+ color: rgba(red, 0.8);
+ }
}
}
}
diff --git a/src/styles/Inputs.scss b/src/styles/Inputs.scss
index 77b82e5..d79117a 100644
--- a/src/styles/Inputs.scss
+++ b/src/styles/Inputs.scss
@@ -11,6 +11,17 @@ button.main-button-primary {
}
}
+// Modals
+.GenericModal button.main-button-primary {
+ background-color: spiceColor("subtext", 0.6) !important;
+ color: spiceColor("main") !important;
+
+ &:hover,
+ &:active {
+ background-color: spiceColor("subtext") !important;
+ }
+}
+
// Checkbox
.x-toggle-indicatorWrapper {
background-color: spiceColor("subtext", 0.1);
diff --git a/src/styles/main.scss b/src/styles/main.scss
index f03e24e..be736e8 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -973,6 +973,7 @@ span.main-userWidget-displayName,
}
.main-rootlist-wrapper > div:nth-child(2) > li img,
+.main-navBar-navBarLink > svg,
.main-navBar-navBarLink > .icon {
z-index: 1;
}
@@ -1055,12 +1056,12 @@ div.GlueDropTarget.personal-library > *.active {
}
html.right-expanded-cover .main-coverSlotExpanded-container {
- right: calc(var(--main-gap) + 10px);
+ right: var(--main-gap);
left: unset;
}
html.right-expanded-cover.buddyfeed-visible .main-coverSlotExpanded-container {
- right: calc(var(--main-gap) + var(--buddy-feed-width) + 10px);
+ right: calc(var(--main-gap) + var(--buddy-feed-width));
left: unset;
}
@@ -1225,6 +1226,7 @@ html.right-expanded-cover.buddyfeed-visible .main-coverSlotExpanded-container {
}
.main-view-container__scroll-node-child {
height: 100%;
+ padding-bottom: 0px;
}
// Hide default Sporify "Offline" notice
diff --git a/src/svg/cog.svg b/src/svg/cog.svg
new file mode 100644
index 0000000..c003872
--- /dev/null
+++ b/src/svg/cog.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file