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