feat(menu): hotswapped profiles w/out reload

This commit is contained in:
dragonwocky 2023-01-19 20:14:51 +11:00
parent 106d776d85
commit 23834475c0
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
4 changed files with 78 additions and 58 deletions

View File

@ -39,7 +39,7 @@ const getProfile = async () => {
const { initDatabase } = globalThis.__enhancerApi, const { initDatabase } = globalThis.__enhancerApi,
db = initDatabase(); db = initDatabase();
let activeProfile = await db.get("activeProfile"); let activeProfile = await db.get("activeProfile");
activeProfile ??= await db.get("profileIds")?.[0]; activeProfile ??= (await db.get("profileIds"))?.[0];
return activeProfile ?? "default"; return activeProfile ?? "default";
}, },
isEnabled = async (id) => { isEnabled = async (id) => {

View File

@ -21,7 +21,7 @@ function _Button(
class="flex gap-[8px] items-center px-[12px] shrink-0 class="flex gap-[8px] items-center px-[12px] shrink-0
rounded-[4px] ${size === "sm" ? "h-[28px]" : "h-[32px]"} rounded-[4px] ${size === "sm" ? "h-[28px]" : "h-[32px]"}
transition duration-[20ms] ${primary transition duration-[20ms] ${primary
? `text-[color:var(--theme--accent-primary_contrast)] ? `text-[color:var(--theme--accent-primary\\_contrast)]
font-medium bg-[color:var(--theme--accent-primary)] font-medium bg-[color:var(--theme--accent-primary)]
hover:bg-[color:var(--theme--accent-primary\\_hover)]` hover:bg-[color:var(--theme--accent-primary\\_hover)]`
: `border-(& [color:var(--theme--fg-border)]) : `border-(& [color:var(--theme--fg-border)])
@ -776,8 +776,8 @@ function Profile({
setName, setName,
isActive, isActive,
setActive, setActive,
exportData, exportJson,
importData, importJson,
...props ...props
}) { }) {
const { html } = globalThis.__enhancerApi, const { html } = globalThis.__enhancerApi,
@ -785,12 +785,8 @@ function Profile({
const file = event.target.files[0], const file = event.target.files[0],
reader = new FileReader(); reader = new FileReader();
reader.onload = async (progress) => { reader.onload = async (progress) => {
try { const res = progress.currentTarget.result;
const res = JSON.parse(progress.currentTarget.result); importJson(res);
importData(res);
} catch {
// throw error
}
}; };
reader.readAsText(file); reader.readAsText(file);
}, },
@ -807,8 +803,8 @@ function Profile({
const $a = html`<a const $a = html`<a
class="hidden" class="hidden"
download="notion-enhancer_${await getName()}_${date}.json" download="notion-enhancer_${await getName()}_${date}.json"
href="data:text/plain;charset=utf-8,${encodeURIComponent( href="data:text/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(await exportData()) await exportJson()
)}" )}"
/>`; />`;
document.body.append($a); document.body.append($a);
@ -818,18 +814,16 @@ function Profile({
return html`<li class="flex items-center my-[14px] gap-[8px]" ...${props}> return html`<li class="flex items-center my-[14px] gap-[8px]" ...${props}>
<${Checkbox} <${Checkbox}
checked=${isActive} ...${{ _get: isActive, _set: setActive }}
disabled=${isActive} onchange=${(event) => (event.target.checked = true)}
...${{ _set: setActive }}
/> />
<${Input} <${Input}
size="md" size="md"
type="text" type="text"
icon="file-cog" icon="file-cog"
onchange=${(event) => setName(event.target.value)} onchange=${(event) => setName(event.target.value)}
onrerender=${($input) => { onrerender=${($input) =>
getName().then((value) => ($input.value = value)); getName().then((value) => ($input.value = value))}
}}
/> />
<${Label} size="sm" icon="import"> <${Label} size="sm" icon="import">
<input <input

View File

@ -4,7 +4,13 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { getState, setState, useState } from "./state.mjs"; import {
getState,
setState,
useState,
setEnabled,
modDatabase,
} from "./state.mjs";
import { import {
Button, Button,
Description, Description,
@ -66,12 +72,11 @@ const renderSidebar = (items, categories) => {
return $sidebar; return $sidebar;
}, },
renderList = async (id, mods, description) => { renderList = async (id, mods, description) => {
const { html, getProfile, initDatabase } = globalThis.__enhancerApi, const { html, isEnabled } = globalThis.__enhancerApi;
enabledMods = initDatabase([await getProfile(), "enabledMods"]);
mods = mods.map(async (mod) => { mods = mods.map(async (mod) => {
const _get = () => enabledMods.get(mod.id), const _get = () => isEnabled(mod.id),
_set = async (enabled) => { _set = async (enabled) => {
await enabledMods.set(mod.id, enabled); await setEnabled(mod.id, enabled);
setState({ rerender: true, databaseUpdated: true }); setState({ rerender: true, databaseUpdated: true });
}; };
return html`<${Mod} ...${{ ...mod, _get, _set }} />`; return html`<${Mod} ...${{ ...mod, _get, _set }} />`;
@ -81,11 +86,8 @@ const renderSidebar = (items, categories) => {
<//>`; <//>`;
}, },
renderOptions = async (mod) => { renderOptions = async (mod) => {
const { html, platform, getProfile } = globalThis.__enhancerApi, const { html, platform, getProfile } = globalThis.__enhancerApi;
{ optionDefaults, initDatabase } = globalThis.__enhancerApi, let options = mod.options.reduce((options, opt) => {
profile = await getProfile(),
db = initDatabase([profile, mod.id], await optionDefaults(mod.id));
let options = mod.options.reduce((options, opt, i) => {
if (!opt.key && (opt.type !== "heading" || !opt.label)) return options; if (!opt.key && (opt.type !== "heading" || !opt.label)) return options;
if (opt.platforms && !opt.platforms.includes(platform)) return options; if (opt.platforms && !opt.platforms.includes(platform)) return options;
const prevOpt = options[options.length - 1]; const prevOpt = options[options.length - 1];
@ -99,9 +101,9 @@ const renderSidebar = (items, categories) => {
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 _get = () => db.get(opt.key), const _get = async () => (await modDatabase(mod.id)).get(opt.key),
_set = async (value) => { _set = async (value) => {
await db.set(opt.key, value); await (await modDatabase(mod.id)).set(opt.key, value);
setState({ rerender: true, databaseUpdated: true }); setState({ rerender: true, databaseUpdated: true });
}; };
return html`<${Option} ...${{ ...opt, _get, _set }} />`; return html`<${Option} ...${{ ...opt, _get, _set }} />`;
@ -109,37 +111,49 @@ const renderSidebar = (items, categories) => {
return Promise.all(options); return Promise.all(options);
}, },
renderProfiles = async () => { renderProfiles = async () => {
const { html, getProfile, initDatabase, reloadApp } =
globalThis.__enhancerApi,
db = initDatabase();
let profileIds; let profileIds;
const $list = html`<ul></ul>`, const { html, initDatabase, getProfile } = globalThis.__enhancerApi,
activeProfile = await getProfile(), db = initDatabase(),
$list = html`<ul></ul>`,
renderProfile = (id) => { renderProfile = (id) => {
const profile = initDatabase([id]); const profile = initDatabase([id]);
return html`<${Profile} return html`<${Profile}
id=${id}
getName=${async () => getName=${async () =>
(await profile.get("profileName")) ?? (await profile.get("profileName")) ??
(id === "default" ? "default" : "")} (id === "default" ? "default" : "")}
setName=${(name) => profile.set("profileName", name)} setName=${(name) => profile.set("profileName", name)}
isActive=${id === activeProfile} isActive=${async () => id === (await getProfile())}
setActive=${async (active) => { setActive=${async () => {
if (!active) return;
await db.set("activeProfile", id); await db.set("activeProfile", id);
reloadApp();
}}
exportData=${profile.export}
importData=${async (data) => {
await profile.import(data);
setState({ rerender: true, databaseUpdated: true }); setState({ rerender: true, databaseUpdated: true });
}} }}
exportJson=${async () => JSON.stringify(await profile.export())}
importJson=${async (json) => {
try {
await profile.import(JSON.parse(json));
setState({ rerender: true, databaseUpdated: true });
} catch {
// error
}
}}
/>`; />`;
}, },
refreshProfiles = async () => { refreshProfiles = async () => {
profileIds = (await db.get("profileIds")) ?? ["default"]; profileIds = await db.get("profileIds");
const profiles = await Promise.all(profileIds.map(renderProfile)); if (!profileIds?.length) profileIds = ["default"];
$list.replaceChildren(...profiles); for (const $profile of $list.children) {
const exists = profileIds.includes($profile.id);
if (!exists) $profile.remove();
}
for (let i = 0; i < profileIds.length; i++) {
const id = profileIds[i];
if (document.getElementById(id)) continue;
const $profile = await renderProfile(id),
$next = document.getElementById(profileIds[i + 1]);
if ($next) $list.insertBefore($profile, $next);
else $list.append($profile);
}
}, },
addProfile = async (name) => { addProfile = async (name) => {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
@ -148,9 +162,7 @@ const renderSidebar = (items, categories) => {
await profile.set("profileName", name); await profile.set("profileName", name);
refreshProfiles(); refreshProfiles();
}; };
useState(["rerender"], () => { useState(["rerender"], () => refreshProfiles());
refreshProfiles();
});
// todo: deleting profiles inc. clearing db keys, // todo: deleting profiles inc. clearing db keys,
// throwing errors on invalid json upload // throwing errors on invalid json upload
@ -186,16 +198,15 @@ const renderSidebar = (items, categories) => {
</div>`; </div>`;
}, },
renderMods = async (mods) => { renderMods = async (mods) => {
const { html, getProfile, initDatabase } = globalThis.__enhancerApi, const { html, isEnabled } = globalThis.__enhancerApi;
enabledMods = initDatabase([await getProfile(), "enabledMods"]);
mods = mods mods = mods
.filter((mod) => { .filter((mod) => {
return mod.options?.filter((opt) => opt.type !== "heading").length; return mod.options?.filter((opt) => opt.type !== "heading").length;
}) })
.map(async (mod) => { .map(async (mod) => {
const _get = () => enabledMods.get(mod.id), const _get = () => isEnabled(mod.id),
_set = async (enabled) => { _set = async (enabled) => {
await enabledMods.set(mod.id, enabled); await setEnabled(mod.id, enabled);
setState({ rerender: true, databaseUpdated: true }); setState({ rerender: true, databaseUpdated: true });
}; };
return html`<${View} id=${mod.id}> return html`<${View} id=${mod.id}>
@ -218,9 +229,10 @@ const render = async () => {
icon: "palette", icon: "palette",
id: "themes", id: "themes",
title: "Themes", title: "Themes",
description: `Themes override Notion's colour schemes. To switch between description: `Themes override Notion's colour schemes. Dark themes require
dark mode and light mode, go to <mark>Settings & members My notifications Notion to be in dark mode and light themes require Notion to be in light
& settings My settings Appearance</mark>.`, mode. To switch between dark mode and light mode, go to <mark>Settings &
members My notifications & settings My settings Appearance</mark>.`,
mods: compatibleMods(await getThemes()), mods: compatibleMods(await getThemes()),
}, },
{ {

View File

@ -21,4 +21,18 @@ const _state = {},
callback(getState(keys)); callback(getState(keys));
}; };
export { setState, useState, getState }; const setEnabled = async (id, enabled) => {
const { getProfile, initDatabase } = globalThis.__enhancerApi;
// prettier-ignore
return await initDatabase([
await getProfile(),
"enabledMods"
]).set(id, enabled);
},
modDatabase = async (id) => {
const { getProfile, initDatabase } = globalThis.__enhancerApi,
{ optionDefaults } = globalThis.__enhancerApi;
return initDatabase([await getProfile(), id], await optionDefaults(id));
};
export { setState, useState, getState, setEnabled, modDatabase };