feat(menu): option values update on view change/page focus, animate view changes

This commit is contained in:
dragonwocky 2023-01-14 21:38:51 +11:00
parent 765e7b738c
commit c95d96cd8e
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
7 changed files with 371 additions and 216 deletions

View File

@ -78,10 +78,15 @@ const initDatabase = (namespace, fallbacks = {}) => {
get: (key) => { get: (key) => {
const fallback = fallbacks[key]; const fallback = fallbacks[key];
key = key.startsWith(namespace) ? key : namespace + key; key = key.startsWith(namespace) ? key : namespace + key;
return select.get(key)?.value ?? fallback; try {
return JSON.parse(select.get(key)?.value);
} catch {
return select.get(key)?.value ?? fallback;
}
}, },
set: (key, value) => { set: (key, value) => {
key = key.startsWith(namespace) ? key : namespace + key; key = key.startsWith(namespace) ? key : namespace + key;
value = JSON.stringify(value);
return select.get(key) === undefined return select.get(key) === undefined
? insert.run(key, value) ? insert.run(key, value)
: update.run(value, key); : update.run(value, key);

View File

@ -21,7 +21,7 @@ export default async (api, db) => {
openMenuHotkey = await db.get("openMenuHotkey"), openMenuHotkey = await db.get("openMenuHotkey"),
menuButtonIconStyle = await db.get("menuButtonIconStyle"), menuButtonIconStyle = await db.get("menuButtonIconStyle"),
loadThemeOverrides = await db.get("loadThemeOverrides"), loadThemeOverrides = await db.get("loadThemeOverrides"),
customStyles = JSON.parse((await db.get("customStyles")) || "{}").content; customStyles = (await db.get("customStyles"))?.content;
// appearance // appearance
@ -54,8 +54,8 @@ export default async (api, db) => {
_notionTheme = notionTheme; _notionTheme = notionTheme;
const msg = { const msg = {
namespace: "notion-enhancer", namespace: "notion-enhancer",
iconStyle: menuButtonIconStyle, theme: notionTheme,
mode: notionTheme, icon: menuButtonIconStyle,
}; };
$menuFrame?.contentWindow.postMessage(msg, "*"); $menuFrame?.contentWindow.postMessage(msg, "*");
} }
@ -64,6 +64,7 @@ export default async (api, db) => {
const openMenu = () => { const openMenu = () => {
updateTheme(true); updateTheme(true);
$menuModal?.setAttribute("open", true); $menuModal?.setAttribute("open", true);
$menuFrame?.contentWindow.focus();
}, },
closeMenu = () => $menuModal?.removeAttribute("open"); closeMenu = () => $menuModal?.removeAttribute("open");
@ -129,6 +130,7 @@ export default async (api, db) => {
}); });
document.querySelector(notionSidebar)?.append($menuButton); document.querySelector(notionSidebar)?.append($menuButton);
window.addEventListener("focus", () => updateTheme(true));
addMutationListener("body", () => { addMutationListener("body", () => {
if ($menuModal?.hasAttribute("open")) updateTheme(); if ($menuModal?.hasAttribute("open")) updateTheme();
}); });

View File

@ -4,7 +4,7 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "./state.mjs"; import { setState, useState, getState } from "./state.mjs";
function Sidebar({}, ...children) { function Sidebar({}, ...children) {
const { html } = globalThis.__enhancerApi; const { html } = globalThis.__enhancerApi;
@ -43,7 +43,7 @@ function SidebarButton({ icon, ...props }, ...children) {
<//>`; <//>`;
if (!props.href) { if (!props.href) {
const id = $el.innerText; const id = $el.innerText;
$el.onclick ??= () => setState({ view: id }); $el.onclick ??= () => setState({ transition: "fade", view: id });
useState(["view"], ([view = "welcome"]) => { useState(["view"], ([view = "welcome"]) => {
const active = view.toLowerCase() === id.toLowerCase(); const active = view.toLowerCase() === id.toLowerCase();
$el.style.background = active ? "var(--theme--bg-hover)" : ""; $el.style.background = active ? "var(--theme--bg-hover)" : "";
@ -55,16 +55,34 @@ function SidebarButton({ icon, ...props }, ...children) {
function View({ id }, ...children) { function View({ id }, ...children) {
const { html } = globalThis.__enhancerApi, const { html } = globalThis.__enhancerApi,
duration = 100,
$el = html`<article $el = html`<article
id=${id} id=${id}
class="notion-enhancer--menu-view h-full class="notion-enhancer--menu-view h-full
overflow-y-auto px-[60px] py-[36px] grow" grow overflow-y-auto px-[60px] py-[36px]"
> >
${children} ${children}
</article>`; </article>`;
useState(["view"], ([view = "welcome"]) => { useState(["view"], ([view = "welcome"]) => {
const active = view.toLowerCase() === id.toLowerCase(); const [transition] = getState(["transition"]),
$el.style.display = active ? "" : "none"; isVisible = $el.style.display !== "none",
nowActive = view.toLowerCase() === id.toLowerCase();
if (transition === "fade") {
$el.style.opacity = "0";
$el.style.transition = `opacity ${duration}ms`;
if (isVisible && !nowActive) {
setTimeout(() => ($el.style.display = "none"), duration);
} else if (!isVisible && nowActive) {
setTimeout(() => {
$el.style.display = "";
requestIdleCallback(() => ($el.style.opacity = "1"));
}, duration);
}
} else {
$el.style.transition = "";
$el.style.opacity = nowActive ? "1" : "0";
$el.style.display = nowActive ? "" : "none";
}
}); });
return $el; return $el;
} }
@ -83,11 +101,12 @@ function Mod({
tags = [], tags = [],
authors, authors,
options = [], options = [],
enabled, _get,
_update, _set,
_src, _src,
}) { }) {
const { html, enhancerUrl } = globalThis.__enhancerApi, const { html, enhancerUrl } = globalThis.__enhancerApi,
toggleId = Math.random().toString(36).slice(2, 5),
$thumbnail = thumbnail $thumbnail = thumbnail
? html`<img ? html`<img
src="${enhancerUrl(`${_src}/${thumbnail}`)}" src="${enhancerUrl(`${_src}/${thumbnail}`)}"
@ -99,16 +118,13 @@ function Mod({
class="flex items-center p-[4px] rounded-[4px] transition class="flex items-center p-[4px] rounded-[4px] transition
text-[color:var(--theme--fg-secondary)] my-auto mr-[8px] text-[color:var(--theme--fg-secondary)] my-auto mr-[8px]
duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]" duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
onclick=${() => { onclick=${() => setState({ transition: "none", view: id })}
// open mod options page
}}
> >
<i class="i-settings w-[18px] h-[18px]"></i> <i class="i-settings w-[18px] h-[18px]"></i>
</button>` </button>`
: ""; : "";
return html`<label return html`<label
id=${id} for=${toggleId}
for="${id}-toggle"
class="notion-enhancer--menu-mod flex items-stretch rounded-[4px] class="notion-enhancer--menu-mod flex items-stretch rounded-[4px]
bg-[color:var(--theme--bg-secondary)] w-full py-[18px] px-[16px] bg-[color:var(--theme--bg-secondary)] w-full py-[18px] px-[16px]
border border-[color:var(--theme--fg-border)] cursor-pointer border border-[color:var(--theme--fg-border)] cursor-pointer
@ -133,7 +149,7 @@ function Mod({
text-[color:var(--theme--fg-secondary)]" text-[color:var(--theme--fg-secondary)]"
innerHTML=${description} innerHTML=${description}
></p> ></p>
<div class="flex gap-x-[8px] text-[12px] leading-[16px]"> <div class="mt-auto flex gap-x-[8px] text-[12px] leading-[16px]">
${authors.map((author) => { ${authors.map((author) => {
return html`<a href=${author.homepage} class="flex items-center"> return html`<a href=${author.homepage} class="flex items-center">
<img src=${author.avatar} alt="" class="h-[12px] rounded-full" /> <img src=${author.avatar} alt="" class="h-[12px] rounded-full" />
@ -145,25 +161,20 @@ function Mod({
<div class="flex ml-auto"> <div class="flex ml-auto">
${$options} ${$options}
<div class="my-auto scale-[1.15]"> <div class="my-auto scale-[1.15]">
<${Toggle} <${Toggle} id=${toggleId} ...${{ _get, _set }} />
id="${id}-toggle"
checked=${enabled}
onchange="${(event) => _update(event.target.checked)}"
/>
</div> </div>
</div> </div>
</label>`; </label>`;
} }
function Option({ type, value, description, _update, ...props }) { function Option({ type, value, description, _get, _set, ...props }) {
const { html } = globalThis.__enhancerApi, const { html } = globalThis.__enhancerApi,
camelToSentenceCase = (string) => camelToSentenceCase = (string) =>
string[0].toUpperCase() + string[0].toUpperCase() +
string.replace(/[A-Z]/g, (match) => ` ${match.toLowerCase()}`).slice(1); string.replace(/[A-Z]/g, (match) => ` ${match.toLowerCase()}`).slice(1);
let $input; let $input;
const label = props.label ?? camelToSentenceCase(props.key), const label = props.label ?? camelToSentenceCase(props.key);
onchange = (event) => _update(event.target.value);
switch (type) { switch (type) {
case "heading": case "heading":
return html`<h4 return html`<h4
@ -174,40 +185,28 @@ function Option({ type, value, description, _update, ...props }) {
${label} ${label}
</h4>`; </h4>`;
case "text": case "text":
$input = html`<${TextInput} ...${{ value, onchange }} />`; $input = html`<${TextInput} ...${{ _get, _set }} />`;
break; break;
case "number": case "number":
$input = html`<${NumberInput} ...${{ value, onchange }} />`; $input = html`<${NumberInput} ...${{ _get, _set }} />`;
break; break;
case "hotkey": case "hotkey":
$input = html`<${HotkeyInput} ...${{ value, onchange }} />`; $input = html`<${HotkeyInput} ...${{ _get, _set }} />`;
break; break;
case "color": case "color":
$input = html`<${ColorInput} ...${{ value, onchange }} />`; $input = html`<${ColorInput} ...${{ _get, _set }} />`;
break; break;
case "file": case "file":
try {
value = JSON.parse(value);
} catch {
value = {};
}
$input = html`<${FileInput} $input = html`<${FileInput}
filename="${value.filename}"
extensions="${props.extensions}" extensions="${props.extensions}"
onupload=${(upload) => _update(JSON.stringify(upload))} ...${{ _get, _set }}
/>`; />`;
break; break;
case "select": case "select":
$input = html`<${Select} $input = html`<${Select} values=${props.values} ...${{ _get, _set }} />`;
values=${props.values}
...${{ value, onchange }}
/>`;
break; break;
case "toggle": case "toggle":
$input = html`<${Toggle} $input = html`<${Toggle} ...${{ _get, _set }} />`;
checked=${value}
onchange=${(event) => _update(event.target.checked)}
/>`;
} }
return html`<${type === "toggle" ? "label" : "div"} return html`<${type === "toggle" ? "label" : "div"}
class="notion-enhancer--menu-option flex items-center justify-between class="notion-enhancer--menu-option flex items-center justify-between
@ -225,19 +224,28 @@ function Option({ type, value, description, _update, ...props }) {
<//>`; <//>`;
} }
function TextInput({ value, ...props }) { function TextInput({ _get, _set, ...props }) {
const { html } = globalThis.__enhancerApi; const { html } = globalThis.__enhancerApi,
return html`<label $input = html`<input
class="notion-enhancer--menu-text-input
relative block w-full mt-[4px] mb-[8px]"
>
<input
type="text" type="text"
class="appearance-none text-[14px] leading-[1.2] rounded-[4px] pb-px class="appearance-none text-[14px] leading-[1.2] rounded-[4px] pb-px
h-[28px] w-full pl-[8px] pr-[30px] bg-[color:var(--theme--bg-hover)]" h-[28px] w-full pl-[8px] pr-[30px] bg-[color:var(--theme--bg-hover)]"
value=${value}
...${props} ...${props}
/> />`;
const { onchange } = $input;
$input.onchange = (event) => {
onchange?.(event);
_set?.($input.value);
};
useState(["rerender"], () => {
_get?.().then((value) => ($input.value = value));
});
return html`<label
class="notion-enhancer--menu-text-input
relative block w-full mt-[4px] mb-[8px]"
>${$input}
<i <i
class="i-text-cursor pointer-events-none class="i-text-cursor pointer-events-none
absolute right-[8px] top-[6px] w-[16px] h-[16px] absolute right-[8px] top-[6px] w-[16px] h-[16px]
@ -246,19 +254,28 @@ function TextInput({ value, ...props }) {
</label>`; </label>`;
} }
function NumberInput({ value, ...props }) { function NumberInput({ _get, _set, ...props }) {
const { html } = globalThis.__enhancerApi; const { html } = globalThis.__enhancerApi,
$input = html`<input
type="text"
class="appearance-none text-[14px] leading-[1.2] rounded-[4px] pb-px
h-[28px] w-full pl-[8px] pr-[32px] bg-[color:var(--theme--bg-hover)]"
...${props}
/>`;
const { onchange } = $input;
$input.onchange = (event) => {
onchange?.(event);
_set?.($input.value);
};
useState(["rerender"], () => {
_get?.().then((value) => ($input.value = value));
});
return html`<label return html`<label
class="notion-enhancer--menu-number-input class="notion-enhancer--menu-number-input
relative shrink-0 w-[192px]" relative shrink-0 w-[192px]"
> >${$input}
<input
type="number"
class="appearance-none text-[14px] leading-[1.2] rounded-[4px] pb-px
h-[28px] w-full pl-[8px] pr-[32px] bg-[color:var(--theme--bg-hover)]"
value=${value}
...${props}
/>
<i <i
class="i-hash pointer-events-none class="i-hash pointer-events-none
absolute right-[8px] top-[6px] w-[16px] h-[16px] absolute right-[8px] top-[6px] w-[16px] h-[16px]
@ -267,8 +284,14 @@ function NumberInput({ value, ...props }) {
</label>`; </label>`;
} }
function HotkeyInput({ value, onkeydown, ...props }) { function HotkeyInput({ _get, _set, ...props }) {
const { html } = globalThis.__enhancerApi, const { html } = globalThis.__enhancerApi,
$input = html`<input
type="text"
class="appearance-none text-[14px] leading-[1.2] rounded-[4px] pb-px
h-[28px] w-full pl-[8px] pr-[32px] bg-[color:var(--theme--bg-hover)]"
...${props}
/>`,
updateHotkey = (event) => { updateHotkey = (event) => {
event.preventDefault(); event.preventDefault();
const keys = []; const keys = [];
@ -278,7 +301,7 @@ function HotkeyInput({ value, onkeydown, ...props }) {
keys.push(alias); keys.push(alias);
} }
if (!keys.length && ["Backspace", "Delete"].includes(event.key)) { if (!keys.length && ["Backspace", "Delete"].includes(event.key)) {
event.target.value = ""; $input.value = "";
} else if (event.key) { } else if (event.key) {
let key = event.key; let key = event.key;
if (key === " ") key = "Space"; if (key === " ") key = "Space";
@ -290,27 +313,26 @@ function HotkeyInput({ value, onkeydown, ...props }) {
// avoid e.g. Shift+Shift, force inclusion of non-modifier // avoid e.g. Shift+Shift, force inclusion of non-modifier
if (keys.includes(key)) return; if (keys.includes(key)) return;
keys.push(key.length === 1 ? key.toUpperCase() : key); keys.push(key.length === 1 ? key.toUpperCase() : key);
event.target.value = keys.join("+"); $input.value = keys.join("+");
} }
event.target.dispatchEvent(new Event("input")); $input.dispatchEvent(new Event("input"));
event.target.dispatchEvent(new Event("change")); $input.dispatchEvent(new Event("change"));
}; };
props.onkeydown = (event) => {
const { onkeydown } = $input;
$input.onkeydown = (event) => {
updateHotkey(event); updateHotkey(event);
onkeydown?.(event); onkeydown?.(event);
_set?.($input.value);
}; };
useState(["rerender"], () => {
_get?.().then((value) => ($input.value = value));
});
return html`<label return html`<label
class="notion-enhancer--menu-hotkey-input class="notion-enhancer--menu-hotkey-input
relative shrink-0 w-[192px]" relative shrink-0 w-[192px]"
> >${$input}
<input
type="text"
class="appearance-none text-[14px] leading-[1.2] rounded-[4px] pb-px
h-[28px] w-full pl-[8px] pr-[32px] bg-[color:var(--theme--bg-hover)]"
value=${value}
...${props}
/>
<i <i
class="i-command pointer-events-none class="i-command pointer-events-none
absolute right-[8px] top-[6px] w-[16px] h-[16px] absolute right-[8px] top-[6px] w-[16px] h-[16px]
@ -319,9 +341,21 @@ function HotkeyInput({ value, onkeydown, ...props }) {
</label>`; </label>`;
} }
function ColorInput({ value, ...props }) { function ColorInput({ _get, _set, ...props }) {
Coloris({ format: "rgb" });
const { html } = globalThis.__enhancerApi, const { html } = globalThis.__enhancerApi,
updateContrast = ($input, $icon) => { $input = html`<input
type="text"
class="appearance-none text-[14px] leading-[1.2]
h-[28px] w-full pl-[8px] pr-[32px] pb-px"
data-coloris
...${props}
/>`,
$icon = html`<i
class="i-pipette pointer-events-none absolute opacity-70
right-[8px] top-[6px] w-[16px] h-[16px] text-current"
></i>`,
updateContrast = () => {
$input.style.background = $input.value; $input.style.background = $input.value;
const [r, g, b, a = 1] = $input.value const [r, g, b, a = 1] = $input.value
.replace(/^rgba?\(/, "") .replace(/^rgba?\(/, "")
@ -337,26 +371,18 @@ function ColorInput({ value, ...props }) {
$icon.style.color = $input.style.color; $icon.style.color = $input.style.color;
}; };
const $input = html`<input const { oninput } = $input;
type="text"
class="appearance-none text-[14px] leading-[1.2]
h-[28px] w-full pl-[8px] pr-[32px] pb-px"
style="background: ${value}"
value=${value}
data-coloris
...${props}
/>`,
$icon = html`<i
class="i-pipette pointer-events-none absolute opacity-70
right-[8px] top-[6px] w-[16px] h-[16px] text-current"
></i>`,
{ oninput } = $input;
$input.oninput = (event) => { $input.oninput = (event) => {
oninput?.(event); oninput?.(event);
updateContrast($input, $icon); _set?.($input.value);
updateContrast();
}; };
updateContrast($input, $icon); useState(["rerender"], () => {
Coloris({ format: "rgb" }); _get?.().then((value) => {
$input.value = value;
updateContrast();
});
});
return html`<label return html`<label
class="notion-enhancer--menu-color-input shrink-0 class="notion-enhancer--menu-color-input shrink-0
@ -365,26 +391,27 @@ function ColorInput({ value, ...props }) {
[position:0_0,4px_4px] [position:0_0,4px_4px]
[size:8px_8px] [size:8px_8px]
)" )"
> >${$input}${$icon}
${$input}${$icon}
</label>`; </label>`;
} }
function FileInput({ filename, extensions, onupload, onchange, ...props }) { function FileInput({ extensions, _get, _set, ...props }) {
const { html } = globalThis.__enhancerApi, const { html } = globalThis.__enhancerApi,
$filename = html`<span>${filename || "Upload a file"}</span>`, $filename = html`<span>Upload a file</span>`,
$clear = html`<button $clear = html`<button
class="ml-[8px] h-[14px] cursor-pointer text-[color:var(--theme--fg-secondary)] class="ml-[8px] h-[14px] cursor-pointer text-[color:var(--theme--fg-secondary)]
transition duration-[20ms] hover:text-[color:var(--theme--fg-primary)]" transition duration-[20ms] hover:text-[color:var(--theme--fg-primary)]"
style=${filename ? "" : "display: none"} style="display: none"
onclick=${() => { onclick=${() => {
$filename.innerText = "Upload a file"; $filename.innerText = "Upload a file";
$clear.style.display = "none"; $clear.style.display = "none";
onupload?.({ filename: "", content: "" }); _set?.({ filename: "", content: "" });
}} }}
> >
<i class="i-x w-[14px] h-[14px]"></i> <i class="i-x w-[14px] h-[14px]"></i>
</button>`; </button>`;
const { onchange } = props;
props.onchange = (event) => { props.onchange = (event) => {
const file = event.target.files[0], const file = event.target.files[0],
reader = new FileReader(); reader = new FileReader();
@ -393,11 +420,18 @@ function FileInput({ filename, extensions, onupload, onchange, ...props }) {
upload = { filename: file.name, content }; upload = { filename: file.name, content };
$filename.innerText = file.name; $filename.innerText = file.name;
$clear.style.display = ""; $clear.style.display = "";
onupload?.(upload); _set?.(upload);
}; };
reader.readAsText(file); reader.readAsText(file);
onchange?.(event); onchange?.(event);
}; };
useState(["rerender"], () => {
_get?.().then((file) => {
$filename.innerText = file?.filename || "Upload a file";
$clear.style.display = file?.filename ? "" : "none";
});
});
return html`<div return html`<div
class="notion-enhancer--menu-file-input shrink-0 flex items-center" class="notion-enhancer--menu-file-input shrink-0 flex items-center"
> >
@ -423,9 +457,25 @@ function FileInput({ filename, extensions, onupload, onchange, ...props }) {
</div>`; </div>`;
} }
function Select({ values, value, onchange, ...props }) { function Select({ values, _get, _set, ...props }) {
const { html } = globalThis.__enhancerApi, const { html } = globalThis.__enhancerApi,
updateWidth = ($select) => { $select = html`<select
class="appearance-none bg-transparent rounded-[4px] cursor-pointer
text-[14px] leading-[1.2] pl-[8px] pr-[28px] h-[28px] max-w-[256px]
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
...${props}
>
${values.map((value) => {
return html`<option
value=${value}
class="bg-[color:var(--theme--bg-secondary)]
text-[color:var(--theme--fg-primary)]"
>
${value}
</option>`;
})}
</select>`,
updateWidth = () => {
const $tmp = html`<span const $tmp = html`<span
class="text-[14px] pl-[8px] pr-[28px] class="text-[14px] pl-[8px] pr-[28px]
absolute top-[-9999px] left-[-9999px]" absolute top-[-9999px] left-[-9999px]"
@ -437,29 +487,19 @@ function Select({ values, value, onchange, ...props }) {
$tmp.remove(); $tmp.remove();
}); });
}; };
props.onchange = (event) => {
onchange?.(event);
updateWidth(event.target);
};
const $select = html`<select const { onchange } = $select;
class="appearance-none bg-transparent rounded-[4px] cursor-pointer $select.onchange = (event) => {
text-[14px] leading-[1.2] pl-[8px] pr-[28px] h-[28px] max-w-[256px] onchange?.(event);
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]" _set?.($select.value);
...${props} updateWidth();
> };
${values.map((value) => { useState(["rerender"], () => {
return html`<option _get?.().then((value) => {
value=${value} $select.value = value;
class="bg-[color:var(--theme--bg-secondary)] updateWidth();
text-[color:var(--theme--fg-primary)]" });
> });
${value}
</option>`;
})}
</select>`;
$select.value = value ?? $select.value;
updateWidth($select);
return html`<div class="notion-enhancer--menu-select relative"> return html`<div class="notion-enhancer--menu-select relative">
${$select} ${$select}
@ -471,16 +511,27 @@ function Select({ values, value, onchange, ...props }) {
</div>`; </div>`;
} }
function Toggle(props) { function Toggle({ _get, _set, ...props }) {
const { html } = globalThis.__enhancerApi; const { html } = globalThis.__enhancerApi,
return html`<div class="notion-enhancer--menu-toggle shrink-0"> $input = html`<input
<input
tabindex="-1" tabindex="-1"
type="checkbox" type="checkbox"
class="hidden checked:sibling:children:( class="hidden checked:sibling:children:(
bg-[color:var(--theme--accent-primary)] after:translate-x-[12px])" bg-[color:var(--theme--accent-primary)] after:translate-x-[12px])"
...${props} ...${props}
/> />`;
const { onchange } = $input;
$input.onchange = (event) => {
onchange?.(event);
_set?.($input.checked);
};
useState(["rerender"], () => {
_get?.().then((checked) => ($input.checked = checked));
});
return html`<div class="notion-enhancer--menu-toggle shrink-0">
${$input}
<div <div
tabindex="0" tabindex="0"
class="w-[30px] h-[18px] rounded-[44px] cursor-pointer class="w-[30px] h-[18px] rounded-[44px] cursor-pointer

View File

@ -4,7 +4,7 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "./state.mjs"; import { getState, setState, useState } from "./state.mjs";
import { import {
Sidebar, Sidebar,
SidebarSection, SidebarSection,
@ -34,19 +34,16 @@ const renderOptions = async (mod) => {
if (options[options.length - 1]?.type === "heading") options.pop(); if (options[options.length - 1]?.type === "heading") options.pop();
options = options.map(async (opt) => { options = options.map(async (opt) => {
if (opt.type === "heading") return html`<${Option} ...${opt} />`; if (opt.type === "heading") return html`<${Option} ...${opt} />`;
const value = await db.get(opt.key), const _get = () => db.get(opt.key),
_update = (value) => db.set(opt.key, value); _set = (value) => db.set(opt.key, value);
return html`<${Option} ...${{ ...opt, value, _update }} />`; return html`<${Option} ...${{ ...opt, _get, _set }} />`;
}); });
return Promise.all(options); return Promise.all(options);
}; };
const renderList = async (mods) => { const compatibleMods = (mods) => {
const { html, platform, getProfile } = globalThis.__enhancerApi, const { platform } = globalThis.__enhancerApi;
{ isEnabled, initDatabase } = globalThis.__enhancerApi, return mods.filter((mod) => {
enabledMods = initDatabase([await getProfile(), "enabledMods"]);
mods = mods
.filter((mod) => {
const required = const required =
mod.id && mod.id &&
mod.name && mod.name &&
@ -56,85 +53,123 @@ const renderList = async (mods) => {
mod.authors, mod.authors,
compatible = !mod.platforms || mod.platforms.includes(platform); compatible = !mod.platforms || mod.platforms.includes(platform);
return required && compatible; return required && compatible;
});
},
renderList = async (mods) => {
const { html, getProfile, initDatabase } = globalThis.__enhancerApi,
enabledMods = initDatabase([await getProfile(), "enabledMods"]);
mods = compatibleMods(mods).map(async (mod) => {
const _get = () => enabledMods.get(mod.id),
_set = (enabled) => enabledMods.set(mod.id, enabled);
return html`<${Mod} ...${{ ...mod, _get, _set }} />`;
});
return html`<${List}>${await Promise.all(mods)}<//>`;
};
const renderOptionViews = async (parentView, mods) => {
const { html, getProfile, initDatabase } = globalThis.__enhancerApi,
enabledMods = initDatabase([await getProfile(), "enabledMods"]);
mods = compatibleMods(mods)
.filter((mod) => {
return mod.options?.filter((opt) => opt.type !== "heading").length;
}) })
.map(async (mod) => { .map(async (mod) => {
const enabled = await isEnabled(mod.id), const _get = () => enabledMods.get(mod.id),
_update = (enabled) => enabledMods.set(mod.id, enabled); _set = (enabled) => enabledMods.set(mod.id, enabled);
return html`<${Mod} ...${{ ...mod, enabled, _update }} />`; return html`<${View} id=${mod.id}>
<${Mod} ...${{ ...mod, options: [], _get, _set }} />
${await renderOptions(mod)}<//
>`;
}); });
return html`<${List}>${await Promise.all(mods)}<//>`; return Promise.all(mods);
}; };
let renderStarted; const render = async () => {
const render = async (iconStyle) => {
const { html, getCore, getThemes } = globalThis.__enhancerApi, const { html, getCore, getThemes } = globalThis.__enhancerApi,
{ getExtensions, getIntegrations } = globalThis.__enhancerApi; { getExtensions, getIntegrations } = globalThis.__enhancerApi,
if (!html || !getCore || renderStarted) return; [icon, renderStarted] = getState(["icon", "renderStarted"]);
renderStarted = true; if (!html || !getCore || !icon || renderStarted) return;
setState({ renderStarted: true });
const $sidebar = html`<${Sidebar}> const sidebar = [
${[ "notion-enhancer",
"notion-enhancer", {
{ icon: `notion-enhancer${icon === "Monochrome" ? "?mask" : ""}`,
icon: `notion-enhancer${iconStyle === "Monochrome" ? "?mask" : ""}`, title: "Welcome",
title: "Welcome", },
}, {
{ icon: "message-circle",
icon: "message-circle", title: "Community",
title: "Community", href: "https://discord.gg/sFWPXtA",
href: "https://discord.gg/sFWPXtA", },
}, {
{ icon: "clock",
icon: "clock", title: "Changelog",
title: "Changelog", href: "https://notion-enhancer.github.io/about/changelog/",
href: "https://notion-enhancer.github.io/about/changelog/", },
}, {
{ icon: "book",
icon: "book", title: "Documentation",
title: "Documentation", href: "https://notion-enhancer.github.io/",
href: "https://notion-enhancer.github.io/", },
}, {
{ icon: "github",
icon: "github", title: "Source Code",
title: "Source Code", href: "https://github.com/notion-enhancer",
href: "https://github.com/notion-enhancer", },
}, {
{ icon: "coffee",
icon: "coffee", title: "Sponsor",
title: "Sponsor", href: "https://github.com/sponsors/dragonwocky",
href: "https://github.com/sponsors/dragonwocky", },
}, "Settings",
"Settings", { icon: "sliders-horizontal", title: "Core" },
{ icon: "sliders-horizontal", title: "Core" }, { icon: "palette", title: "Themes" },
{ icon: "palette", title: "Themes" }, { icon: "zap", title: "Extensions" },
{ icon: "zap", title: "Extensions" }, { icon: "plug", title: "Integrations" },
{ icon: "plug", title: "Integrations" }, ],
].map((item) => { $sidebar = html`<${Sidebar}>
if (typeof item === "string") { ${sidebar.map((item) => {
return html`<${SidebarSection}>${item}<//>`; if (typeof item === "object") {
} else {
const { title, ...props } = item; const { title, ...props } = item;
return html`<${SidebarButton} ...${props}>${title}<//>`; return html`<${SidebarButton} ...${props}>${title}<//>`;
} } else return html`<${SidebarSection}>${item}<//>`;
})} })}
<//>`, <//>`;
$views = [ document.body.append(
html`<${View} id="welcome">welcome<//>`, $sidebar,
html`<${View} id="core">${await renderOptions(await getCore())}<//>`, html`<${View} id="welcome">welcome<//>`,
html`<${View} id="themes">${await renderList(await getThemes())}<//>`, html`<${View} id="core">${await renderOptions(await getCore())}<//>`
html`<${View} id="extensions"> );
${await renderList(await getExtensions())} for (const { id, mods } of [
<//>`, { id: "themes", mods: await getThemes() },
html`<${View} id="integrations"> { id: "extensions", mods: await getExtensions() },
${await renderList(await getIntegrations())} { id: "integrations", mods: await getIntegrations() },
<//>`, ]) {
]; document.body.append(
document.body.append($sidebar, ...$views); html`<${View} id=${id}>${await renderList(mods)}<//>`,
...(await renderOptionViews(id, mods))
);
}
}; };
window.addEventListener("message", async (event) => { window.addEventListener("focus", () => setState({ rerender: true }));
window.addEventListener("message", (event) => {
if (event.data?.namespace !== "notion-enhancer") return; if (event.data?.namespace !== "notion-enhancer") return;
setState({ theme: event.data?.mode }); const [theme, icon] = getState(["theme", "icon"]);
setState({
rerender: true,
theme: event.data?.theme ?? theme,
icon: event.data?.icon ?? icon,
});
});
useState(["theme"], ([theme]) => {
if (theme === "dark") document.body.classList.add("dark");
if (theme === "light") document.body.classList.remove("dark");
});
useState(["rerender"], async () => {
const [theme, icon] = getState(["theme", "icon"]);
if (!theme || !icon) return;
// chrome extensions run in an isolated execution context // chrome extensions run in an isolated execution context
// but extension:// pages can access chrome apis // but extension:// pages can access chrome apis
// ∴ notion-enhancer api is imported directly // ∴ notion-enhancer api is imported directly
@ -148,9 +183,5 @@ window.addEventListener("message", async (event) => {
// load stylesheets from enabled themes // load stylesheets from enabled themes
await import("../../load.mjs"); await import("../../load.mjs");
// wait for api globals to be available // wait for api globals to be available
requestIdleCallback(() => render(event.data?.iconStyle)); requestIdleCallback(() => render());
});
useState(["theme"], ([mode]) => {
if (mode === "dark") document.body.classList.add("dark");
if (mode === "light") document.body.classList.remove("dark");
}); });

View File

@ -6,6 +6,9 @@
const _state = {}, const _state = {},
_subscribers = [], _subscribers = [],
getState = (keys) => {
return keys.map((key) => _state[key]);
},
setState = (state) => { setState = (state) => {
Object.assign(_state, state); Object.assign(_state, state);
const updates = Object.keys(state); const updates = Object.keys(state);
@ -15,7 +18,7 @@ const _state = {},
}, },
useState = (keys, callback) => { useState = (keys, callback) => {
_subscribers.push([keys, callback]); _subscribers.push([keys, callback]);
callback(keys.map((key) => _state[key])); callback(getState(keys));
}; };
export { setState, useState }; export { setState, useState, getState };

View File

@ -26,6 +26,12 @@
"type": "heading", "type": "heading",
"label": "Appearance" "label": "Appearance"
}, },
{
"type": "toggle",
"key": "toggle",
"description": "Sets whether the notion-enhancer icon added to Notion's sidebar should be coloured or monochrome. The latter style will match the theme's icon colour for users who would like the icon to be less noticeable.",
"value": true
},
{ {
"type": "select", "type": "select",
"key": "menuButtonIconStyle", "key": "menuButtonIconStyle",
@ -54,6 +60,18 @@
"description": "Activates built-in debugging tools accessible through the application menu.", "description": "Activates built-in debugging tools accessible through the application menu.",
"platforms": ["darwin", "win32", "linux"], "platforms": ["darwin", "win32", "linux"],
"value": false "value": false
},
{
"type": "text",
"key": "text",
"description": "Activates built-in debugging tools accessible through the application menu.",
"value": ""
},
{
"type": "number",
"key": "number",
"description": "Activates built-in debugging tools accessible through the application menu.",
"value": ""
} }
], ],
"clientStyles": ["variables.css"], "clientStyles": ["variables.css"],

View File

@ -12,5 +12,50 @@
"avatar": "https://dragonwocky.me/avatar.jpg" "avatar": "https://dragonwocky.me/avatar.jpg"
} }
], ],
"options": [
{
"type": "heading",
"label": "Hotkeys"
},
{
"type": "hotkey",
"key": "openMenuHotkey",
"description": "Opens the notion-enhancer menu from within Notion.",
"value": "Ctrl+Shift+,"
},
{
"type": "heading",
"label": "Appearance"
},
{
"type": "select",
"key": "menuButtonIconStyle",
"description": "Sets whether the notion-enhancer icon added to Notion's sidebar should be coloured or monochrome. The latter style will match the theme's icon colour for users who would like the icon to be less noticeable.",
"values": ["Colour", "Monochrome"]
},
{
"type": "select",
"key": "loadThemeOverrides",
"description": "Loads the styling required for a theme to customise Notion's interface. Disabling this will increase client performance.",
"values": ["Auto", "Enabled", "Disabled"]
},
{
"type": "file",
"key": "customStyles",
"description": "Adds the styles from an uploaded .css file to Notion. Use this if you would like to customise the current theme or <a href=\"https://notion-enhancer.github.io/advanced/tweaks\">otherwise tweak Notion's appearance</a>.",
"extensions": ["css"]
},
{
"type": "heading",
"label": "Advanced"
},
{
"type": "toggle",
"key": "debugMode",
"description": "Activates built-in debugging tools accessible through the application menu.",
"platforms": ["darwin", "win32", "linux"],
"value": false
}
],
"clientStyles": ["client.css"] "clientStyles": ["client.css"]
} }