mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-11 15:59:03 +00:00
refactor(menu): menu.mjs render functions and components.mjs monolith -> islands/ and components/
This commit is contained in:
parent
c19262a4ce
commit
e3f34dfc21
@ -538,8 +538,10 @@ const h = (type, props, ...children) => {
|
|||||||
: document.createElement(type);
|
: document.createElement(type);
|
||||||
for (const prop in props ?? {}) {
|
for (const prop in props ?? {}) {
|
||||||
if (htmlAttributes.includes(prop) || prop.startsWith("data-")) {
|
if (htmlAttributes.includes(prop) || prop.startsWith("data-")) {
|
||||||
if (typeof props[prop] === "boolean" && !props[prop]) continue;
|
if (typeof props[prop] === "boolean") {
|
||||||
elem.setAttribute(prop, props[prop]);
|
if (!props[prop]) continue;
|
||||||
|
elem.setAttribute(prop, "");
|
||||||
|
} else elem.setAttribute(prop, props[prop]);
|
||||||
} else elem[prop] = props[prop];
|
} else elem[prop] = props[prop];
|
||||||
}
|
}
|
||||||
elem.append(...children);
|
elem.append(...children);
|
||||||
|
124
src/api/mods.js
124
src/api/mods.js
@ -6,89 +6,75 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
let _mods;
|
const _isManifestValid = (modManifest) => {
|
||||||
const getMods = async () => {
|
const hasRequiredFields =
|
||||||
const { readJson } = globalThis.__enhancerApi;
|
modManifest.id &&
|
||||||
_mods ??= await Promise.all(
|
modManifest.name &&
|
||||||
// prettier-ignore
|
modManifest.version &&
|
||||||
(await readJson("registry.json")).map(async (_src) => {
|
modManifest.description &&
|
||||||
const modManifest = await readJson(`${_src}/mod.json`);
|
modManifest.authors,
|
||||||
return { ...modManifest, _src };
|
meetsThemeRequirements =
|
||||||
})
|
!modManifest._src.startsWith("themes/") ||
|
||||||
);
|
((modManifest.tags?.includes("dark") ||
|
||||||
return _mods;
|
modManifest.tags?.includes("light")) &&
|
||||||
},
|
modManifest.thumbnail),
|
||||||
getCore = async () => {
|
targetsCurrentPlatform =
|
||||||
const mods = await getMods();
|
!modManifest.platforms || //
|
||||||
return mods.find(({ _src }) => _src === "core");
|
modManifest.platforms.includes(platform);
|
||||||
},
|
return hasRequiredFields && meetsThemeRequirements && targetsCurrentPlatform;
|
||||||
getThemes = async () => {
|
};
|
||||||
const mods = await getMods();
|
|
||||||
return mods.filter(({ _src }) => _src.startsWith("themes/"));
|
|
||||||
},
|
|
||||||
getExtensions = async () => {
|
|
||||||
const mods = await getMods();
|
|
||||||
return mods.filter(({ _src }) => _src.startsWith("extensions/"));
|
|
||||||
},
|
|
||||||
getIntegrations = async () => {
|
|
||||||
const mods = await getMods();
|
|
||||||
return mods.filter(({ _src }) => _src.startsWith("integrations/"));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProfile = async () => {
|
let _mods;
|
||||||
const { initDatabase } = globalThis.__enhancerApi,
|
const getMods = async (category) => {
|
||||||
db = initDatabase();
|
const { readJson } = globalThis.__enhancerApi;
|
||||||
|
// prettier-ignore
|
||||||
|
_mods ??= (await Promise.all((await readJson("registry.json")).map(async (_src) => {
|
||||||
|
const modManifest = { ...(await readJson(`${_src}/mod.json`)), _src };
|
||||||
|
return _isManifestValid(modManifest) ? modManifest : undefined;
|
||||||
|
}))).filter((mod) => mod);
|
||||||
|
return category
|
||||||
|
? _mods.filter(({ _src }) => {
|
||||||
|
return _src === category || _src.startsWith(`${category}/`);
|
||||||
|
})
|
||||||
|
: _mods;
|
||||||
|
},
|
||||||
|
getProfile = async () => {
|
||||||
|
const db = globalThis.__enhancerApi.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) => {
|
|
||||||
const { platform } = globalThis.__enhancerApi,
|
|
||||||
mod = (await getMods()).find((mod) => mod.id === id);
|
|
||||||
if (mod._src === "core") return true;
|
|
||||||
if (mod.platforms && !mod.platforms.includes(platform)) return false;
|
|
||||||
const { initDatabase } = globalThis.__enhancerApi,
|
|
||||||
enabledMods = initDatabase([await getProfile(), "enabledMods"]);
|
|
||||||
return Boolean(await enabledMods.get(id));
|
|
||||||
},
|
|
||||||
setEnabled = async (id, enabled) => {
|
|
||||||
const { initDatabase } = globalThis.__enhancerApi;
|
|
||||||
// prettier-ignore
|
|
||||||
return await initDatabase([
|
|
||||||
await getProfile(),
|
|
||||||
"enabledMods"
|
|
||||||
]).set(id, enabled);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const optionDefaults = async (id) => {
|
const isEnabled = async (id) => {
|
||||||
const mod = (await getMods()).find((mod) => mod.id === id),
|
const mod = (await getMods()).find((mod) => mod.id === id);
|
||||||
optionEntries = mod.options
|
// prettier-ignore
|
||||||
.map((opt) => {
|
return mod._src === "core" || await globalThis.__enhancerApi
|
||||||
if (
|
.initDatabase([await getProfile(), "enabledMods"])
|
||||||
["toggle", "text", "number", "hotkey", "color"].includes(opt.type)
|
.get(id);
|
||||||
)
|
|
||||||
return [opt.key, opt.value];
|
|
||||||
if (opt.type === "select") return [opt.key, opt.values[0]];
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
.filter((opt) => opt);
|
|
||||||
return Object.fromEntries(optionEntries);
|
|
||||||
},
|
},
|
||||||
modDatabase = async (id) => {
|
setEnabled = async (id, enabled) => {
|
||||||
const { initDatabase } = globalThis.__enhancerApi;
|
return await globalThis.__enhancerApi
|
||||||
return initDatabase([await getProfile(), id], await optionDefaults(id));
|
.initDatabase([await getProfile(), "enabledMods"])
|
||||||
|
.set(id, enabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const modDatabase = async (id) => {
|
||||||
|
// prettier-ignore
|
||||||
|
const optionDefaults = (await getMods())
|
||||||
|
.find((mod) => mod.id === id)?.options
|
||||||
|
.map((opt) => [opt.key, opt.value ?? opt.values?.[0]])
|
||||||
|
.filter(([, value]) => typeof value !== "undefined");
|
||||||
|
return globalThis.__enhancerApi.initDatabase(
|
||||||
|
[await getProfile(), id],
|
||||||
|
Object.fromEntries(optionDefaults)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
globalThis.__enhancerApi ??= {};
|
globalThis.__enhancerApi ??= {};
|
||||||
Object.assign(globalThis.__enhancerApi, {
|
Object.assign(globalThis.__enhancerApi, {
|
||||||
getMods,
|
getMods,
|
||||||
getCore,
|
|
||||||
getThemes,
|
|
||||||
getExtensions,
|
|
||||||
getIntegrations,
|
|
||||||
getProfile,
|
getProfile,
|
||||||
isEnabled,
|
isEnabled,
|
||||||
setEnabled,
|
setEnabled,
|
||||||
optionDefaults,
|
|
||||||
modDatabase,
|
modDatabase,
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ export default async (api, db) => {
|
|||||||
const {
|
const {
|
||||||
html,
|
html,
|
||||||
platform,
|
platform,
|
||||||
getThemes,
|
getMods,
|
||||||
isEnabled,
|
isEnabled,
|
||||||
enhancerUrl,
|
enhancerUrl,
|
||||||
onMessage,
|
onMessage,
|
||||||
@ -25,7 +25,9 @@ export default async (api, db) => {
|
|||||||
|
|
||||||
// appearance
|
// appearance
|
||||||
|
|
||||||
const enabledThemes = (await getThemes()).map((theme) => isEnabled(theme.id)),
|
const enabledThemes = (await getMods("themes")).map((theme) =>
|
||||||
|
isEnabled(theme.id)
|
||||||
|
),
|
||||||
forceLoadOverrides = loadThemeOverrides === "Enabled",
|
forceLoadOverrides = loadThemeOverrides === "Enabled",
|
||||||
autoLoadOverrides =
|
autoLoadOverrides =
|
||||||
loadThemeOverrides === "Auto" &&
|
loadThemeOverrides === "Auto" &&
|
||||||
|
@ -1,937 +0,0 @@
|
|||||||
/**
|
|
||||||
* notion-enhancer
|
|
||||||
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
|
||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { setState, useState, getState } from "./state.mjs";
|
|
||||||
|
|
||||||
// generic
|
|
||||||
|
|
||||||
function _Button(
|
|
||||||
{ type, size, variant, icon, class: cls = "", ...props },
|
|
||||||
...children
|
|
||||||
) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
iconSize =
|
|
||||||
size === "sm" && children.length
|
|
||||||
? "w-[14px] h-[14px]"
|
|
||||||
: "w-[18px] h-[18px]";
|
|
||||||
return html`<${type}
|
|
||||||
class="flex gap-[8px] items-center px-[12px] shrink-0
|
|
||||||
rounded-[4px] ${size === "sm" ? "h-[28px]" : "h-[32px]"}
|
|
||||||
transition duration-[20ms] ${variant === "primary"
|
|
||||||
? `text-[color:var(--theme--accent-primary\\_contrast)]
|
|
||||||
font-medium bg-[color:var(--theme--accent-primary)]
|
|
||||||
hover:bg-[color:var(--theme--accent-primary\\_hover)]`
|
|
||||||
: variant === "secondary"
|
|
||||||
? `text-[color:var(--theme--accent-secondary)]
|
|
||||||
border-(& [color:var(--theme--accent-secondary)])
|
|
||||||
hover:bg-[color:var(--theme--accent-secondary\\_hover)]`
|
|
||||||
: `border-(& [color:var(--theme--fg-border)])
|
|
||||||
hover:bg-[color:var(--theme--bg-hover)]`} ${cls}"
|
|
||||||
...${props}
|
|
||||||
>
|
|
||||||
${icon ? html`<i class="i-${icon} ${iconSize}"></i>` : ""}
|
|
||||||
<span class="text-[${size === "sm" ? "13" : "14"}px] empty:hidden">
|
|
||||||
${children}
|
|
||||||
</span>
|
|
||||||
<//>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Button(props, ...children) {
|
|
||||||
const { html } = globalThis.__enhancerApi;
|
|
||||||
return html`<${_Button} type="button" ...${props}>${children}<//>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Label(props, ...children) {
|
|
||||||
const { html } = globalThis.__enhancerApi;
|
|
||||||
return html`<${_Button} type="label" ...${props}>${children}<//>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Description({ class: cls = "", ...props }, ...children) {
|
|
||||||
const { html } = globalThis.__enhancerApi;
|
|
||||||
return html`<p
|
|
||||||
class="notion-enhancer--menu-description leading-[16px]
|
|
||||||
text-([12px] [color:var(--theme--fg-secondary)]) ${cls}"
|
|
||||||
...${props}
|
|
||||||
>
|
|
||||||
${children}
|
|
||||||
</p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Icon({ icon, ...props }) {
|
|
||||||
const { html } = globalThis.__enhancerApi;
|
|
||||||
return html`<button
|
|
||||||
class="h-[14px] transition duration-[20ms]
|
|
||||||
text-[color:var(--theme--fg-secondary)]
|
|
||||||
hover:text-[color:var(--theme--fg-primary)]"
|
|
||||||
...${props}
|
|
||||||
>
|
|
||||||
<i class="i-${icon} w-[14px] h-[14px]"></i>
|
|
||||||
</button>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// layout
|
|
||||||
|
|
||||||
function Sidebar({}, ...children) {
|
|
||||||
const { html } = globalThis.__enhancerApi;
|
|
||||||
return html`<aside
|
|
||||||
class="notion-enhancer--menu-sidebar z-10 row-span-1
|
|
||||||
h-full overflow-y-auto bg-[color:var(--theme--bg-secondary)]"
|
|
||||||
>
|
|
||||||
${children}
|
|
||||||
</aside>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarSection({}, ...children) {
|
|
||||||
const { html } = globalThis.__enhancerApi;
|
|
||||||
return html`<h2
|
|
||||||
class="text-([11px] [color:var(--theme--fg-secondary)])
|
|
||||||
py-[5px] px-[15px] mb-px mt-[18px] first:mt-[10px]
|
|
||||||
uppercase font-medium tracking-[0.03em] leading-none"
|
|
||||||
>
|
|
||||||
${children}
|
|
||||||
</h2>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarButton({ id, icon, ...props }, ...children) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
$icon = icon
|
|
||||||
? html`<i
|
|
||||||
class="i-${icon} ${icon.startsWith("notion-enhancer")
|
|
||||||
? "w-[17px] h-[17px] ml-[1.5px] mr-[9.5px]"
|
|
||||||
: "w-[18px] h-[18px] ml-px mr-[9px]"}"
|
|
||||||
></i>`
|
|
||||||
: "",
|
|
||||||
$el = html`<${props.href ? "a" : "button"}
|
|
||||||
class="flex select-none cursor-pointer w-full
|
|
||||||
items-center py-[5px] px-[15px] text-[14px] last:mb-[12px]
|
|
||||||
transition hover:bg-[color:var(--theme--bg-hover)]"
|
|
||||||
...${props}
|
|
||||||
>${$icon}
|
|
||||||
<span class="leading-[20px]">${children}</span>
|
|
||||||
<//>`;
|
|
||||||
if (!props.href) {
|
|
||||||
$el.onclick ??= () => setState({ transition: "fade", view: id });
|
|
||||||
useState(["view"], ([view = "welcome"]) => {
|
|
||||||
const active = view.toLowerCase() === id.toLowerCase();
|
|
||||||
$el.style.background = active ? "var(--theme--bg-hover)" : "";
|
|
||||||
$el.style.fontWeight = active ? "600" : "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return $el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function List({ id, description }, ...children) {
|
|
||||||
const { html } = globalThis.__enhancerApi;
|
|
||||||
return html`<div class="flex flex-col gap-y-[14px]">
|
|
||||||
<${Search} type=${id} items=${children} />
|
|
||||||
<${Description} innerHTML=${description} />
|
|
||||||
${children}
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Footer({}, ...children) {
|
|
||||||
const { html } = globalThis.__enhancerApi;
|
|
||||||
return html`<div
|
|
||||||
class="flex w-full px-[60px] py-[16px]
|
|
||||||
border-t-(& [color:var(--theme--fg-border)])
|
|
||||||
bg-[color:var(--theme--bg-primary)]"
|
|
||||||
>
|
|
||||||
${children}
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function View({ id }, ...children) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
$el = html`<article
|
|
||||||
id=${id}
|
|
||||||
class="notion-enhancer--menu-view h-full w-full
|
|
||||||
absolute overflow-y-auto px-[60px] py-[36px]"
|
|
||||||
>
|
|
||||||
${children}
|
|
||||||
</article>`;
|
|
||||||
useState(["view"], ([view = "welcome"]) => {
|
|
||||||
const [transition] = getState(["transition"]),
|
|
||||||
isVisible = $el.style.display !== "none",
|
|
||||||
nowActive = view.toLowerCase() === id.toLowerCase();
|
|
||||||
switch (transition) {
|
|
||||||
case "fade": {
|
|
||||||
const duration = 100,
|
|
||||||
cssTransition = `opacity ${duration}ms`;
|
|
||||||
if (isVisible && !nowActive) {
|
|
||||||
$el.style.transition = cssTransition;
|
|
||||||
$el.style.opacity = "0";
|
|
||||||
setTimeout(() => ($el.style.display = "none"), duration);
|
|
||||||
} else if (!isVisible && nowActive) {
|
|
||||||
setTimeout(() => {
|
|
||||||
$el.style.opacity = "0";
|
|
||||||
$el.style.display = "";
|
|
||||||
requestIdleCallback(() => {
|
|
||||||
$el.style.transition = cssTransition;
|
|
||||||
$el.style.opacity = "1";
|
|
||||||
});
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "slide-to-left":
|
|
||||||
case "slide-to-right": {
|
|
||||||
const duration = 200,
|
|
||||||
cssTransition = `opacity ${duration}ms, transform ${duration}ms`,
|
|
||||||
transformOut = `translateX(${
|
|
||||||
transition === "slide-to-right" ? "-100%" : "100%"
|
|
||||||
})`,
|
|
||||||
transformIn = `translateX(${
|
|
||||||
transition === "slide-to-right" ? "100%" : "-100%"
|
|
||||||
})`;
|
|
||||||
if (isVisible && !nowActive) {
|
|
||||||
$el.style.transition = cssTransition;
|
|
||||||
$el.style.transform = transformOut;
|
|
||||||
$el.style.opacity = "0";
|
|
||||||
setTimeout(() => {
|
|
||||||
$el.style.display = "none";
|
|
||||||
$el.style.transform = "";
|
|
||||||
}, duration);
|
|
||||||
} else if (!isVisible && nowActive) {
|
|
||||||
$el.style.transform = transformIn;
|
|
||||||
$el.style.opacity = "0";
|
|
||||||
$el.style.display = "";
|
|
||||||
requestIdleCallback(() => {
|
|
||||||
$el.style.transition = cssTransition;
|
|
||||||
$el.style.transform = "";
|
|
||||||
$el.style.opacity = "1";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
$el.style.transition = "";
|
|
||||||
$el.style.opacity = nowActive ? "1" : "0";
|
|
||||||
$el.style.display = nowActive ? "" : "none";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return $el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Popup(
|
|
||||||
{ for: $trigger, onopen, onclose, onbeforeclose, ...props },
|
|
||||||
...children
|
|
||||||
) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
$popup = html`<div
|
|
||||||
class="notion-enhancer--menu-popup
|
|
||||||
group absolute top-0 left-0 w-full h-full
|
|
||||||
flex flex-col justify-center items-end
|
|
||||||
pointer-events-none z-20"
|
|
||||||
...${props}
|
|
||||||
>
|
|
||||||
<div class="relative right-[100%]">
|
|
||||||
<div
|
|
||||||
class="bg-[color:var(--theme--bg-secondary)]
|
|
||||||
w-[250px] max-w-[calc(100vw-24px)] max-h-[70vh]
|
|
||||||
py-[6px] px-[4px] drop-shadow-xl overflow-y-auto
|
|
||||||
transition duration-[200ms] opacity-0 scale-95 rounded-[4px]
|
|
||||||
group-open:(pointer-events-auto opacity-100 scale-100)"
|
|
||||||
>
|
|
||||||
${children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const { onclick, onkeydown } = $trigger,
|
|
||||||
enableTabbing = () => {
|
|
||||||
$popup
|
|
||||||
.querySelectorAll("[tabindex]")
|
|
||||||
.forEach(($el) => ($el.tabIndex = 0));
|
|
||||||
},
|
|
||||||
disableTabbing = () => {
|
|
||||||
$popup
|
|
||||||
.querySelectorAll("[tabindex]")
|
|
||||||
.forEach(($el) => ($el.tabIndex = -1));
|
|
||||||
},
|
|
||||||
openPopup = () => {
|
|
||||||
$popup.setAttribute("open", true);
|
|
||||||
enableTabbing();
|
|
||||||
onopen?.();
|
|
||||||
setState({ popupOpen: true });
|
|
||||||
},
|
|
||||||
closePopup = () => {
|
|
||||||
$popup.removeAttribute("open");
|
|
||||||
disableTabbing();
|
|
||||||
onbeforeclose?.();
|
|
||||||
setTimeout(() => {
|
|
||||||
onclose?.();
|
|
||||||
setState({ popupOpen: false });
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
disableTabbing();
|
|
||||||
$trigger.onclick = (event) => {
|
|
||||||
onclick?.(event);
|
|
||||||
openPopup();
|
|
||||||
};
|
|
||||||
$trigger.onkeydown = (event) => {
|
|
||||||
onkeydown?.(event);
|
|
||||||
if (event.key === "Enter") openPopup();
|
|
||||||
};
|
|
||||||
useState(["rerender"], () => {
|
|
||||||
if ($popup.hasAttribute("open")) closePopup();
|
|
||||||
});
|
|
||||||
document.addEventListener("click", (event) => {
|
|
||||||
if (!$popup.hasAttribute("open")) return;
|
|
||||||
if ($popup.contains(event.target) || $popup === event.target) return;
|
|
||||||
if ($trigger.contains(event.target) || $trigger === event.target) return;
|
|
||||||
closePopup();
|
|
||||||
});
|
|
||||||
|
|
||||||
return $popup;
|
|
||||||
}
|
|
||||||
|
|
||||||
// input
|
|
||||||
|
|
||||||
function Input({
|
|
||||||
size,
|
|
||||||
icon,
|
|
||||||
transparent,
|
|
||||||
onrerender,
|
|
||||||
class: cls = "",
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
$input = html`<input
|
|
||||||
class="${size === "lg"
|
|
||||||
? "h-[36px] pl-[12px] pr-[40px]"
|
|
||||||
: "h-[28px] pl-[8px] pr-[32px]"}
|
|
||||||
w-full pb-px text-[14px] leading-[1.2]
|
|
||||||
appearance-none bg-transparent"
|
|
||||||
...${props}
|
|
||||||
/>`,
|
|
||||||
$icon = html`<i
|
|
||||||
class="i-${icon} absolute w-[16px] h-[16px] pointer-events-none
|
|
||||||
${size === "lg" ? "right-[12px] top-[10px]" : "right-[8px] top-[6px]"}
|
|
||||||
text-[color:var(--theme--fg-secondary)]"
|
|
||||||
></i>`;
|
|
||||||
useState(["rerender"], () => onrerender?.($input, $icon));
|
|
||||||
return html`<label
|
|
||||||
focus=${() => $input.focus()}
|
|
||||||
class="notion-enhancer--menu-input
|
|
||||||
relative overflow-hidden rounded-[4px]
|
|
||||||
focus-within:ring-(& [color:var(--theme--accent-primary)])
|
|
||||||
${size === "lg" ? "h-[36px] block w-full" : ""}
|
|
||||||
${size === "md" ? "h-[28px] block w-full" : ""}
|
|
||||||
${size === "sm" ? "h-[28px] shrink-0 w-[192px]" : ""}
|
|
||||||
bg-${transparent
|
|
||||||
? `([image:repeating-linear-gradient(45deg,#aaa_25%,transparent_25%,transparent_75%,#aaa_75%,#aaa),repeating-linear-gradient(45deg,#aaa_25%,#fff_25%,#fff_75%,#aaa_75%,#aaa)]
|
|
||||||
[position:0_0,4px_4px] [size:8px_8px])`
|
|
||||||
: "[color:var(--theme--bg-hover)]"} ${cls}"
|
|
||||||
>${$input}${$icon}
|
|
||||||
</label>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TextInput({ _get, _set, onchange, ...props }) {
|
|
||||||
const { html } = globalThis.__enhancerApi;
|
|
||||||
return html`<${Input}
|
|
||||||
size="md"
|
|
||||||
type="text"
|
|
||||||
icon="text-cursor"
|
|
||||||
class="mt-[4px] mb-[8px]"
|
|
||||||
onchange=${(event) => {
|
|
||||||
onchange?.(event);
|
|
||||||
_set?.(event.target.value);
|
|
||||||
}}
|
|
||||||
onrerender=${($input) => {
|
|
||||||
_get?.().then((value) => ($input.value = value));
|
|
||||||
}}
|
|
||||||
...${props}
|
|
||||||
/>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function NumberInput({ _get, _set, onchange, ...props }) {
|
|
||||||
const { html } = globalThis.__enhancerApi;
|
|
||||||
return html`<${Input}
|
|
||||||
size="sm"
|
|
||||||
type="number"
|
|
||||||
icon="hash"
|
|
||||||
onchange=${(event) => {
|
|
||||||
onchange?.(event);
|
|
||||||
_set?.(event.target.value);
|
|
||||||
}}
|
|
||||||
onrerender=${($input) => {
|
|
||||||
_get?.().then((value) => ($input.value = value));
|
|
||||||
}}
|
|
||||||
...${props}
|
|
||||||
/>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function HotkeyInput({ _get, _set, onkeydown, ...props }) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
updateHotkey = (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const keys = [];
|
|
||||||
for (const modifier of ["metaKey", "ctrlKey", "altKey", "shiftKey"]) {
|
|
||||||
if (!event[modifier]) continue;
|
|
||||||
const alias = modifier[0].toUpperCase() + modifier.slice(1, -3);
|
|
||||||
keys.push(alias);
|
|
||||||
}
|
|
||||||
if (!keys.length && ["Backspace", "Delete"].includes(event.key)) {
|
|
||||||
event.target.value = "";
|
|
||||||
} else if (event.key) {
|
|
||||||
let key = event.key;
|
|
||||||
if (key === " ") key = "Space";
|
|
||||||
if (["+", "="].includes(key)) key = "Plus";
|
|
||||||
if (key === "-") key = "Minus";
|
|
||||||
if (event.code === "Comma") key = ",";
|
|
||||||
if (event.code === "Period") key = ".";
|
|
||||||
if (key === "Control") key = "Ctrl";
|
|
||||||
// avoid e.g. Shift+Shift, force inclusion of non-modifier
|
|
||||||
if (keys.includes(key)) return;
|
|
||||||
keys.push(key.length === 1 ? key.toUpperCase() : key);
|
|
||||||
event.target.value = keys.join("+");
|
|
||||||
}
|
|
||||||
event.target.dispatchEvent(new Event("input"));
|
|
||||||
event.target.dispatchEvent(new Event("change"));
|
|
||||||
};
|
|
||||||
return html`<${Input}
|
|
||||||
size="sm"
|
|
||||||
type="text"
|
|
||||||
icon="command"
|
|
||||||
onkeydown=${(event) => {
|
|
||||||
updateHotkey(event);
|
|
||||||
onkeydown?.(event);
|
|
||||||
_set?.(event.target.value);
|
|
||||||
}}
|
|
||||||
onrerender=${($input) => {
|
|
||||||
_get?.().then((value) => ($input.value = value));
|
|
||||||
}}
|
|
||||||
...${props}
|
|
||||||
/>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ColorInput({ _get, _set, oninput, ...props }) {
|
|
||||||
Coloris({ format: "rgb" });
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
updateContrast = ($input, $icon) => {
|
|
||||||
$input.style.background = $input.value;
|
|
||||||
const [r, g, b, a = 1] = $input.value
|
|
||||||
.replace(/^rgba?\(/, "")
|
|
||||||
.replace(/\)$/, "")
|
|
||||||
.split(",")
|
|
||||||
.map((n) => parseFloat(n));
|
|
||||||
if (a > 0.5) {
|
|
||||||
// pick a contrasting foreground for an rgb background
|
|
||||||
// using the percieved brightness constants from http://alienryderflex.com/hsp.html
|
|
||||||
const brightness = 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b);
|
|
||||||
$input.style.color = Math.sqrt(brightness) > 165.75 ? "#000" : "#fff";
|
|
||||||
} else $input.style.color = "#000";
|
|
||||||
$icon.style.color = $input.style.color;
|
|
||||||
$icon.style.opacity = "0.7";
|
|
||||||
};
|
|
||||||
return html`<${Input}
|
|
||||||
transparent
|
|
||||||
size="sm"
|
|
||||||
type="text"
|
|
||||||
icon="pipette"
|
|
||||||
data-coloris
|
|
||||||
oninput=${(event) => {
|
|
||||||
oninput?.(event);
|
|
||||||
_set?.(event.target.value);
|
|
||||||
}}
|
|
||||||
onrerender=${($input, $icon) => {
|
|
||||||
_get?.().then((value) => {
|
|
||||||
$input.value = value;
|
|
||||||
updateContrast($input, $icon);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
...${props}
|
|
||||||
/>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FileInput({ extensions, _get, _set, ...props }) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
$filename = html`<span>Upload a file</span>`,
|
|
||||||
$clear = html`<${Icon}
|
|
||||||
icon="x"
|
|
||||||
style="display: none"
|
|
||||||
onclick=${() => {
|
|
||||||
$filename.innerText = "Upload a file";
|
|
||||||
$clear.style.display = "none";
|
|
||||||
_set?.({ filename: "", content: "" });
|
|
||||||
}}
|
|
||||||
/>`;
|
|
||||||
|
|
||||||
const { onchange } = props;
|
|
||||||
props.onchange = (event) => {
|
|
||||||
const file = event.target.files[0],
|
|
||||||
reader = new FileReader();
|
|
||||||
reader.onload = async (progress) => {
|
|
||||||
const content = progress.currentTarget.result,
|
|
||||||
upload = { filename: file.name, content };
|
|
||||||
$filename.innerText = file.name;
|
|
||||||
$clear.style.display = "";
|
|
||||||
_set?.(upload);
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
onchange?.(event);
|
|
||||||
};
|
|
||||||
useState(["rerender"], () => {
|
|
||||||
_get?.().then((file) => {
|
|
||||||
$filename.innerText = file?.filename || "Upload a file";
|
|
||||||
$clear.style.display = file?.filename ? "" : "none";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`<div
|
|
||||||
class="notion-enhancer--menu-file-input
|
|
||||||
shrink-0 flex items-center gap-[8px]"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
tabindex="0"
|
|
||||||
class="flex items-center cursor-pointer select-none
|
|
||||||
h-[28px] px-[8px] bg-[color:var(--theme--bg-secondary)]
|
|
||||||
text-([14px] [color:var(--theme--fg-secondary)]) rounded-[4px]
|
|
||||||
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
class="hidden"
|
|
||||||
accept=${extensions
|
|
||||||
?.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
|
|
||||||
.join(",")}
|
|
||||||
...${props}
|
|
||||||
/>
|
|
||||||
<i class="i-file-up w-[16px] h-[16px] mr-[6px]"></i>
|
|
||||||
${$filename}
|
|
||||||
</label>
|
|
||||||
${$clear}
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Select({ values, _get, _set, ...props }) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
$select = html`<div
|
|
||||||
dir="rtl"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
class="appearance-none bg-transparent rounded-[4px] cursor-pointer
|
|
||||||
text-[14px] leading-[28px] h-[28px] max-w-[256px] pl-[8px] pr-[28px]
|
|
||||||
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
|
||||||
...${props}
|
|
||||||
></div>`,
|
|
||||||
$options = values.map((value) => {
|
|
||||||
return html`<${SelectOption} ...${{ value, _get, _set }} />`;
|
|
||||||
});
|
|
||||||
useState(["rerender"], () => {
|
|
||||||
_get?.().then((value) => ($select.innerText = value));
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`<div class="notion-enhancer--menu-select relative">
|
|
||||||
${$select}
|
|
||||||
<${Popup}
|
|
||||||
for=${$select}
|
|
||||||
onbeforeclose=${() => {
|
|
||||||
$select.style.width = `${$select.offsetWidth}px`;
|
|
||||||
$select.style.background = "transparent";
|
|
||||||
}}
|
|
||||||
onclose=${() => {
|
|
||||||
$select.style.width = "";
|
|
||||||
$select.style.background = "";
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${$options}
|
|
||||||
<//>
|
|
||||||
<i
|
|
||||||
class="i-chevron-down pointer-events-none
|
|
||||||
absolute right-[6px] top-[6px] w-[16px] h-[16px]
|
|
||||||
text-[color:var(--theme--fg-secondary)]"
|
|
||||||
></i>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectOption({ value, _get, _set, ...props }) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
$selected = html`<i class="ml-auto i-check w-[16px] h-[16px]"></i>`,
|
|
||||||
$option = html`<div
|
|
||||||
tabindex="0"
|
|
||||||
role="button"
|
|
||||||
class="select-none cursor-pointer rounded-[3px]
|
|
||||||
flex items-center w-full h-[28px] px-[12px] leading-[1.2]
|
|
||||||
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
|
||||||
...${props}
|
|
||||||
>
|
|
||||||
<div class="mr-[6px] text-[14px] text-ellipsis overflow-hidden">
|
|
||||||
${value}
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const { onclick, onkeydown } = $option;
|
|
||||||
$option.onclick = (event) => {
|
|
||||||
onclick?.(event);
|
|
||||||
_set?.(value);
|
|
||||||
};
|
|
||||||
$option.onkeydown = (event) => {
|
|
||||||
onkeydown?.(event);
|
|
||||||
if (event.key === "Enter") _set?.(value);
|
|
||||||
};
|
|
||||||
useState(["rerender"], () => {
|
|
||||||
_get?.().then((actualValue) => {
|
|
||||||
if (actualValue === value) {
|
|
||||||
$option.append($selected);
|
|
||||||
} else $selected.remove();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return $option;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Toggle({ _get, _set, ...props }) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
$input = html`<input
|
|
||||||
type="checkbox"
|
|
||||||
class="hidden checked:sibling:children:(
|
|
||||||
bg-[color:var(--theme--accent-primary)] after:translate-x-[12px])"
|
|
||||||
...${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
|
|
||||||
tabindex="0"
|
|
||||||
class="w-[30px] h-[18px] rounded-[44px] cursor-pointer
|
|
||||||
transition duration-200 bg-[color:var(--theme--bg-hover)]"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-full h-full rounded-[44px] text-[12px]
|
|
||||||
p-[2px] hover:bg-[color:var(--theme--bg-hover)]
|
|
||||||
transition duration-200 after:(
|
|
||||||
inline-block w-[14px] h-[14px] rounded-[44px]
|
|
||||||
bg-[color:var(--theme--accent-primary\\_contrast)]
|
|
||||||
transition duration-200
|
|
||||||
)"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Checkbox({ _get, _set, ...props }) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
$input = html`<input
|
|
||||||
type="checkbox"
|
|
||||||
class="hidden checked:sibling:(px-[1px]
|
|
||||||
bg-[color:var(--theme--accent-primary)])
|
|
||||||
not-checked:sibling:(children:text-transparent
|
|
||||||
border-(& [color:var(--theme--fg-primary)])
|
|
||||||
hover:bg-[color:var(--theme--bg-hover)])"
|
|
||||||
...${props}
|
|
||||||
/>`;
|
|
||||||
|
|
||||||
const { onchange } = $input;
|
|
||||||
$input.onchange = (event) => {
|
|
||||||
onchange?.(event);
|
|
||||||
_set?.($input.checked);
|
|
||||||
};
|
|
||||||
useState(["rerender"], () => {
|
|
||||||
_get?.().then((checked) => ($input.checked = checked));
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`<label tabindex="0" class="cursor-pointer">
|
|
||||||
${$input}
|
|
||||||
<div class="flex items-center h-[16px] transition duration-[200ms]">
|
|
||||||
<i
|
|
||||||
class="i-check w-[14px] h-[14px]
|
|
||||||
text-[color:var(--theme--accent-primary\\_contrast)]"
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
</label>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Search({ type, items, oninput, ...props }) {
|
|
||||||
const { html, addKeyListener } = globalThis.__enhancerApi,
|
|
||||||
$search = html`<${Input}
|
|
||||||
size="lg"
|
|
||||||
type="text"
|
|
||||||
placeholder="Search ${items.length} ${items.length === 1
|
|
||||||
? type.replace(/s$/, "")
|
|
||||||
: type} (Press '/' to focus)"
|
|
||||||
icon="search"
|
|
||||||
oninput=${(event) => {
|
|
||||||
oninput?.(event);
|
|
||||||
const query = event.target.value.toLowerCase();
|
|
||||||
for (const $item of items) {
|
|
||||||
const matches = $item.innerText.toLowerCase().includes(query);
|
|
||||||
$item.style.display = matches ? "" : "none";
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
...${props}
|
|
||||||
/>`;
|
|
||||||
addKeyListener("/", (event) => {
|
|
||||||
if (document.activeElement?.nodeName === "INPUT") return;
|
|
||||||
// offsetParent == null if parent has "display: none;"
|
|
||||||
if ($search.offsetParent) {
|
|
||||||
event.preventDefault();
|
|
||||||
$search.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return $search;
|
|
||||||
}
|
|
||||||
|
|
||||||
// representative
|
|
||||||
|
|
||||||
function Mod({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
version,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
tags = [],
|
|
||||||
authors,
|
|
||||||
options = [],
|
|
||||||
_get,
|
|
||||||
_set,
|
|
||||||
_src,
|
|
||||||
}) {
|
|
||||||
const { html, enhancerUrl } = globalThis.__enhancerApi,
|
|
||||||
toggleId = Math.random().toString(36).slice(2, 5),
|
|
||||||
$thumbnail = thumbnail
|
|
||||||
? html`<img
|
|
||||||
src="${enhancerUrl(`${_src}/${thumbnail}`)}"
|
|
||||||
class="rounded-[4px] mr-[12px] h-[74px] my-auto"
|
|
||||||
/>`
|
|
||||||
: "",
|
|
||||||
$options = options.length
|
|
||||||
? html`<button
|
|
||||||
class="flex items-center p-[4px] rounded-[4px] transition
|
|
||||||
text-[color:var(--theme--fg-secondary)] my-auto mr-[8px]
|
|
||||||
duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]
|
|
||||||
active:text-[color:var(--theme--fg-primary)]"
|
|
||||||
onclick=${() => setState({ transition: "slide-to-right", view: id })}
|
|
||||||
>
|
|
||||||
<i class="i-settings w-[18px] h-[18px]"></i>
|
|
||||||
</button>`
|
|
||||||
: "";
|
|
||||||
return html`<label
|
|
||||||
for=${toggleId}
|
|
||||||
class="notion-enhancer--menu-mod flex items-stretch rounded-[4px]
|
|
||||||
bg-[color:var(--theme--bg-secondary)] w-full py-[18px] px-[16px]
|
|
||||||
border border-[color:var(--theme--fg-border)] cursor-pointer
|
|
||||||
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
|
||||||
>
|
|
||||||
${$thumbnail}
|
|
||||||
<div class="flex flex-col max-w-[50%]">
|
|
||||||
<div class="flex items-center text-[14px] mb-[5px]">
|
|
||||||
<h3 class="my-0">${name}</h3>
|
|
||||||
${[`v${version}`, ...tags].map((tag) => {
|
|
||||||
return html`<span
|
|
||||||
class="text-([12px] [color:var(--theme--fg-secondary)])
|
|
||||||
ml-[8px] py-[2px] px-[6px] leading-tight tracking-wide
|
|
||||||
rounded-[3px] bg-[color:var(--theme--bg-hover)]"
|
|
||||||
>
|
|
||||||
${tag}
|
|
||||||
</span>`;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<${Description} class="mb-[6px]" innerHTML=${description} />
|
|
||||||
<div class="mt-auto flex gap-x-[8px] text-[12px] leading-[16px]">
|
|
||||||
${authors.map((author) => {
|
|
||||||
return html`<a href=${author.homepage} class="flex items-center">
|
|
||||||
<img src=${author.avatar} alt="" class="h-[12px] rounded-full" />
|
|
||||||
<span class="ml-[6px]">${author.name}</span>
|
|
||||||
</a>`;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex ml-auto">
|
|
||||||
${$options}
|
|
||||||
<div class="my-auto scale-[1.15]">
|
|
||||||
<${Toggle} id=${toggleId} ...${{ _get, _set }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Option({ type, value, description, _get, _set, ...props }) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
camelToSentenceCase = (string) =>
|
|
||||||
string[0].toUpperCase() +
|
|
||||||
string.replace(/[A-Z]/g, (match) => ` ${match.toLowerCase()}`).slice(1);
|
|
||||||
|
|
||||||
let $input;
|
|
||||||
const label = props.label ?? camelToSentenceCase(props.key);
|
|
||||||
switch (type) {
|
|
||||||
case "heading":
|
|
||||||
return html`<h4
|
|
||||||
class="notion-enhancer--menu-heading font-semibold
|
|
||||||
mb-[16px] mt-[48px] first:mt-0 pb-[12px] text-[16px]
|
|
||||||
border-b-(& [color:var(--theme--fg-border)])"
|
|
||||||
>
|
|
||||||
${label}
|
|
||||||
</h4>`;
|
|
||||||
case "text":
|
|
||||||
$input = html`<${TextInput} ...${{ _get, _set }} />`;
|
|
||||||
break;
|
|
||||||
case "number":
|
|
||||||
$input = html`<${NumberInput} ...${{ _get, _set }} />`;
|
|
||||||
break;
|
|
||||||
case "hotkey":
|
|
||||||
$input = html`<${HotkeyInput} ...${{ _get, _set }} />`;
|
|
||||||
break;
|
|
||||||
case "color":
|
|
||||||
$input = html`<${ColorInput} ...${{ _get, _set }} />`;
|
|
||||||
break;
|
|
||||||
case "file":
|
|
||||||
$input = html`<${FileInput}
|
|
||||||
extensions="${props.extensions}"
|
|
||||||
...${{ _get, _set }}
|
|
||||||
/>`;
|
|
||||||
break;
|
|
||||||
case "select":
|
|
||||||
$input = html`<${Select} values=${props.values} ...${{ _get, _set }} />`;
|
|
||||||
break;
|
|
||||||
case "toggle":
|
|
||||||
$input = html`<${Toggle} ...${{ _get, _set }} />`;
|
|
||||||
}
|
|
||||||
return html`<${type === "toggle" ? "label" : "div"}
|
|
||||||
class="notion-enhancer--menu-option flex items-center justify-between
|
|
||||||
mb-[18px] ${type === "toggle" ? "cursor-pointer" : ""}"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col ${type === "text" ? "w-full" : "mr-[10%]"}">
|
|
||||||
<h5 class="text-[14px] mb-[2px] mt-0">${label}</h5>
|
|
||||||
${type === "text" ? $input : ""}
|
|
||||||
<${Description} innerHTML=${description} />
|
|
||||||
</div>
|
|
||||||
${type === "text" ? "" : $input}
|
|
||||||
<//>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Profile({
|
|
||||||
getName,
|
|
||||||
setName,
|
|
||||||
isActive,
|
|
||||||
setActive,
|
|
||||||
exportJson,
|
|
||||||
importJson,
|
|
||||||
deleteProfile,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const { html } = globalThis.__enhancerApi,
|
|
||||||
uploadProfile = (event) => {
|
|
||||||
const file = event.target.files[0],
|
|
||||||
reader = new FileReader();
|
|
||||||
reader.onload = async (progress) => {
|
|
||||||
const res = progress.currentTarget.result;
|
|
||||||
importJson(res);
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
},
|
|
||||||
downloadProfile = async () => {
|
|
||||||
const now = new Date(),
|
|
||||||
year = now.getFullYear().toString(),
|
|
||||||
month = (now.getMonth() + 1).toString().padStart(2, "0"),
|
|
||||||
day = now.getDate().toString().padStart(2, "0"),
|
|
||||||
hour = now.getHours().toString().padStart(2, "0"),
|
|
||||||
min = now.getMinutes().toString().padStart(2, "0"),
|
|
||||||
sec = now.getSeconds().toString().padStart(2, "0"),
|
|
||||||
date = year + month + day + hour + min + sec;
|
|
||||||
|
|
||||||
const $a = html`<a
|
|
||||||
class="hidden"
|
|
||||||
download="notion-enhancer_${await getName()}_${date}.json"
|
|
||||||
href="data:text/json;charset=utf-8,${encodeURIComponent(
|
|
||||||
await exportJson()
|
|
||||||
)}"
|
|
||||||
/>`;
|
|
||||||
document.body.append($a);
|
|
||||||
$a.click();
|
|
||||||
$a.remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
const $delete = html`<${Icon} icon="x" />`,
|
|
||||||
$name = html`<mark></mark>`,
|
|
||||||
$confirmation = html`<${Popup}
|
|
||||||
for=${$delete}
|
|
||||||
onopen=${async () => ($name.innerText = await getName())}
|
|
||||||
>
|
|
||||||
<p class="text-[14px] pt-[2px] px-[8px]">
|
|
||||||
Are you sure you want to delete the profile ${$name} permanently?
|
|
||||||
</p>
|
|
||||||
<div class="flex flex-col gap-[8px] pt-[8px] pb-[6px] px-[8px]">
|
|
||||||
<${Button}
|
|
||||||
tabindex="0"
|
|
||||||
icon="trash"
|
|
||||||
class="justify-center"
|
|
||||||
variant="secondary"
|
|
||||||
onclick=${() => deleteProfile()}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
<//>
|
|
||||||
<${Button}
|
|
||||||
tabindex="0"
|
|
||||||
class="justify-center"
|
|
||||||
onclick=${() => setState({ rerender: true })}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
<//>
|
|
||||||
</div>
|
|
||||||
<//>`;
|
|
||||||
|
|
||||||
return html`<li class="flex items-center my-[14px] gap-[8px]" ...${props}>
|
|
||||||
<${Checkbox}
|
|
||||||
...${{ _get: isActive, _set: setActive }}
|
|
||||||
onchange=${(event) => (event.target.checked = true)}
|
|
||||||
/>
|
|
||||||
<${Input}
|
|
||||||
size="md"
|
|
||||||
type="text"
|
|
||||||
icon="file-cog"
|
|
||||||
onchange=${(event) => setName(event.target.value)}
|
|
||||||
onrerender=${($input) =>
|
|
||||||
getName().then((value) => ($input.value = value))}
|
|
||||||
/>
|
|
||||||
<${Label} size="sm" icon="import">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
class="hidden"
|
|
||||||
accept=".json"
|
|
||||||
onchange=${uploadProfile}
|
|
||||||
/>
|
|
||||||
Import
|
|
||||||
<//>
|
|
||||||
<${Button} size="sm" icon="upload" onclick=${downloadProfile}>Export<//>
|
|
||||||
<div class="relative">${$delete}${$confirmation}</div>
|
|
||||||
</li>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Button,
|
|
||||||
Label,
|
|
||||||
Description,
|
|
||||||
Icon,
|
|
||||||
Sidebar,
|
|
||||||
SidebarSection,
|
|
||||||
SidebarButton,
|
|
||||||
List,
|
|
||||||
Footer,
|
|
||||||
View,
|
|
||||||
Input,
|
|
||||||
TextInput,
|
|
||||||
NumberInput,
|
|
||||||
HotkeyInput,
|
|
||||||
ColorInput,
|
|
||||||
FileInput,
|
|
||||||
Select,
|
|
||||||
Toggle,
|
|
||||||
Checkbox,
|
|
||||||
Search,
|
|
||||||
Mod,
|
|
||||||
Option,
|
|
||||||
Profile,
|
|
||||||
};
|
|
41
src/core/menu/components/Button.mjs
Normal file
41
src/core/menu/components/Button.mjs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extendProps } from "../state.mjs";
|
||||||
|
|
||||||
|
function Button({ icon, variant, tagName, ...props }, ...children) {
|
||||||
|
const { html } = globalThis.__enhancerApi;
|
||||||
|
extendProps(props, {
|
||||||
|
class: `notion-enhancer--menu-button shrink-0
|
||||||
|
flex gap-[8px] items-center px-[12px] rounded-[4px]
|
||||||
|
h-[${variant === "sm" ? "28" : "32"}px] transition
|
||||||
|
duration-[20ms] ${
|
||||||
|
variant === "primary"
|
||||||
|
? `text-[color:var(--theme--accent-primary\\_contrast)]
|
||||||
|
font-medium bg-[color:var(--theme--accent-primary)]
|
||||||
|
hover:bg-[color:var(--theme--accent-primary\\_hover)]`
|
||||||
|
: variant === "secondary"
|
||||||
|
? `text-[color:var(--theme--accent-secondary)]
|
||||||
|
border-(& [color:var(--theme--accent-secondary)])
|
||||||
|
hover:bg-[color:var(--theme--accent-secondary\\_hover)]`
|
||||||
|
: `border-(& [color:var(--theme--fg-border)])
|
||||||
|
hover:bg-[color:var(--theme--bg-hover)]`
|
||||||
|
}`,
|
||||||
|
});
|
||||||
|
return html`<${tagName ?? "button"} ...${props}>
|
||||||
|
${icon
|
||||||
|
? html`<i
|
||||||
|
class="i-${icon}
|
||||||
|
text-[${variant === "sm" && children.length ? "14" : "18"}px]"
|
||||||
|
></i>`
|
||||||
|
: ""}
|
||||||
|
<span class="text-[${variant === "sm" ? "13" : "14"}px] empty:hidden">
|
||||||
|
${children}
|
||||||
|
</span>
|
||||||
|
<//>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button };
|
39
src/core/menu/components/Checkbox.mjs
Normal file
39
src/core/menu/components/Checkbox.mjs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, extendProps } from "../state.mjs";
|
||||||
|
|
||||||
|
function Checkbox({ _get, _set, ...props }) {
|
||||||
|
const { html } = globalThis.__enhancerApi,
|
||||||
|
$input = html`<input
|
||||||
|
type="checkbox"
|
||||||
|
class="hidden checked:sibling:(px-[1px]
|
||||||
|
bg-[color:var(--theme--accent-primary)])
|
||||||
|
not-checked:sibling:(children:text-transparent
|
||||||
|
border-(& [color:var(--theme--fg-primary)])
|
||||||
|
hover:bg-[color:var(--theme--bg-hover)])"
|
||||||
|
...${props}
|
||||||
|
/>`;
|
||||||
|
extendProps($input, { onchange: () => _set?.($input.checked) });
|
||||||
|
useState(["rerender"], () => {
|
||||||
|
_get?.().then((checked) => ($input.checked = checked));
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`<label
|
||||||
|
tabindex="0"
|
||||||
|
class="notion-enhancer--menu-checkbox cursor-pointer"
|
||||||
|
>
|
||||||
|
${$input}
|
||||||
|
<div class="flex items-center h-[16px] transition duration-[200ms]">
|
||||||
|
<i
|
||||||
|
class="i-check w-[14px] h-[14px]
|
||||||
|
text-[color:var(--theme--accent-primary\\_contrast)]"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</label>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox };
|
18
src/core/menu/components/Description.mjs
Normal file
18
src/core/menu/components/Description.mjs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extendProps } from "../state.mjs";
|
||||||
|
|
||||||
|
function Description(props, ...children) {
|
||||||
|
const { html } = globalThis.__enhancerApi;
|
||||||
|
extendProps(props, {
|
||||||
|
class: `notion-enhancer--menu-description leading-[16px]
|
||||||
|
text-([12px] [color:var(--theme--fg-secondary)])`,
|
||||||
|
});
|
||||||
|
return html`<p ...${props}>${children}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Description };
|
19
src/core/menu/components/Heading.mjs
Normal file
19
src/core/menu/components/Heading.mjs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extendProps } from "../state.mjs";
|
||||||
|
|
||||||
|
function Heading(props, children) {
|
||||||
|
const { html } = globalThis.__enhancerApi;
|
||||||
|
extendProps(props, {
|
||||||
|
class: `notion-enhancer--menu-heading font-semibold
|
||||||
|
mb-[16px] mt-[48px] first:mt-0 pb-[12px] text-[16px]
|
||||||
|
border-b-(& [color:var(--theme--fg-border)])`,
|
||||||
|
});
|
||||||
|
return html`<h4 ...${props}>${children}</h4>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Heading };
|
165
src/core/menu/components/Input.mjs
Normal file
165
src/core/menu/components/Input.mjs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extendProps, useState } from "../state.mjs";
|
||||||
|
|
||||||
|
const updateHotkey = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const keys = [];
|
||||||
|
for (const modifier of ["metaKey", "ctrlKey", "altKey", "shiftKey"]) {
|
||||||
|
if (!event[modifier]) continue;
|
||||||
|
const alias = modifier[0].toUpperCase() + modifier.slice(1, -3);
|
||||||
|
keys.push(alias);
|
||||||
|
}
|
||||||
|
if (!keys.length && ["Backspace", "Delete"].includes(event.key)) {
|
||||||
|
event.target.value = "";
|
||||||
|
} else if (event.key) {
|
||||||
|
let key = event.key;
|
||||||
|
if (key === " ") key = "Space";
|
||||||
|
if (["+", "="].includes(key)) key = "Plus";
|
||||||
|
if (key === "-") key = "Minus";
|
||||||
|
if (event.code === "Comma") key = ",";
|
||||||
|
if (event.code === "Period") key = ".";
|
||||||
|
if (key === "Control") key = "Ctrl";
|
||||||
|
// avoid e.g. Shift+Shift, force inclusion of non-modifier
|
||||||
|
if (keys.includes(key)) return;
|
||||||
|
keys.push(key.length === 1 ? key.toUpperCase() : key);
|
||||||
|
event.target.value = keys.join("+");
|
||||||
|
}
|
||||||
|
event.target.dispatchEvent(new Event("input"));
|
||||||
|
event.target.dispatchEvent(new Event("change"));
|
||||||
|
},
|
||||||
|
updateContrast = ($input, $icon) => {
|
||||||
|
$input.style.background = $input.value;
|
||||||
|
const [r, g, b, a = 1] = $input.value
|
||||||
|
.replace(/^rgba?\(/, "")
|
||||||
|
.replace(/\)$/, "")
|
||||||
|
.split(",")
|
||||||
|
.map((n) => parseFloat(n));
|
||||||
|
if (a > 0.5) {
|
||||||
|
// pick a contrasting foreground for an rgb background
|
||||||
|
// using the percieved brightness constants from http://alienryderflex.com/hsp.html
|
||||||
|
const brightness = 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b);
|
||||||
|
$input.style.color = Math.sqrt(brightness) > 165.75 ? "#000" : "#fff";
|
||||||
|
} else $input.style.color = "#000";
|
||||||
|
$icon.style.color = $input.style.color;
|
||||||
|
$icon.style.opacity = "0.7";
|
||||||
|
},
|
||||||
|
readUpload = async (event) => {
|
||||||
|
const file = event.target.files[0],
|
||||||
|
reader = new FileReader();
|
||||||
|
return new Promise((res) => {
|
||||||
|
reader.onload = async (progress) => {
|
||||||
|
const content = progress.currentTarget.result,
|
||||||
|
upload = { filename: file.name, content };
|
||||||
|
res(upload);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function Input({
|
||||||
|
type,
|
||||||
|
icon,
|
||||||
|
variant,
|
||||||
|
extensions,
|
||||||
|
class: className,
|
||||||
|
_get,
|
||||||
|
_set,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
let $filename, $clear;
|
||||||
|
const { html } = globalThis.__enhancerApi;
|
||||||
|
Coloris({ format: "rgb" });
|
||||||
|
|
||||||
|
type ??= "text";
|
||||||
|
if (type === "text") icon ??= "text-cursor";
|
||||||
|
if (type === "number") icon ??= "hash";
|
||||||
|
if (type === "hotkey") icon ??= "command";
|
||||||
|
if (type === "color") icon ??= "pipette";
|
||||||
|
|
||||||
|
if (type === "file") {
|
||||||
|
icon ??= "file-up";
|
||||||
|
$filename = html`<span class="ml-[6px]">Upload a file</span>`;
|
||||||
|
$clear = html`<button
|
||||||
|
style="display: none"
|
||||||
|
class="h-[14px] transition duration-[20ms]
|
||||||
|
text-[color:var(--theme--fg-secondary)]
|
||||||
|
hover:text-[color:var(--theme--fg-primary)]"
|
||||||
|
onclick=${() => _set?.({ filename: "", content: "" })}
|
||||||
|
>
|
||||||
|
<i class="i-x w-[14px] h-[14px]"></i>
|
||||||
|
</button>`;
|
||||||
|
props.accept = extensions
|
||||||
|
?.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
|
||||||
|
.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
const $input = html`<input
|
||||||
|
type=${["hotkey", "color"].includes(type) ? "text" : type}
|
||||||
|
class="h-full w-full pb-px text-[14px] leading-[1.2]
|
||||||
|
${variant === "lg" ? "pl-[12px] pr-[40px]" : "pl-[8px] pr-[32px]"}
|
||||||
|
appearance-none bg-transparent ${type === "file" ? "hidden" : ""}
|
||||||
|
${type === "color" ? "font-medium" : ""}"
|
||||||
|
data-coloris=${type === "color"}
|
||||||
|
...${props}
|
||||||
|
/>`,
|
||||||
|
$icon = html`<span
|
||||||
|
class="${variant === "lg" ? "pr-[12px]" : "pr-[8px]"}
|
||||||
|
absolute flex items-center h-full pointer-events-none
|
||||||
|
text-[color:var(--theme--fg-secondary)] right-0 top-0"
|
||||||
|
><i class="i-${icon} w-[16px] h-[16px]"></i>
|
||||||
|
</span>`;
|
||||||
|
|
||||||
|
extendProps($input, {
|
||||||
|
onchange: (event) => {
|
||||||
|
if (_set && type === "file") {
|
||||||
|
readUpload(event).then(_set);
|
||||||
|
} else _set?.($input.value);
|
||||||
|
},
|
||||||
|
onrerender: async () => {
|
||||||
|
_get?.().then((value) => {
|
||||||
|
value ??= "";
|
||||||
|
if (type === "file") {
|
||||||
|
$filename.innerText = value?.filename || "Upload a file";
|
||||||
|
$clear.style.display = value?.filename ? "" : "none";
|
||||||
|
} else if ($input.value !== value) $input.value = value;
|
||||||
|
if (type === "color") updateContrast($input, $icon);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onkeydown: type === "hotkey" ? updateHotkey : undefined,
|
||||||
|
oninput: type === "color" ? () => _set?.($input.value) : undefined,
|
||||||
|
});
|
||||||
|
useState(["rerender"], () => $input.onrerender?.());
|
||||||
|
|
||||||
|
return type === "file"
|
||||||
|
? html`<div
|
||||||
|
class="notion-enhancer--menu-file-input shrink-0
|
||||||
|
flex items-center gap-[8px] ${className ?? ""}"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
class="flex items-center cursor-pointer select-none
|
||||||
|
h-[28px] px-[8px] bg-[color:var(--theme--bg-secondary)]
|
||||||
|
text-([14px] [color:var(--theme--fg-secondary)]) rounded-[4px]
|
||||||
|
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
||||||
|
>${$input}${$icon.children[0]}${$filename}
|
||||||
|
</label>
|
||||||
|
${$clear}
|
||||||
|
</div>`
|
||||||
|
: html`<label
|
||||||
|
class="notion-enhancer--menu-input
|
||||||
|
${variant === "lg" ? "h-[32px]" : "h-[28px]"}
|
||||||
|
relative overflow-hidden rounded-[4px] w-full inline-block
|
||||||
|
focus-within:ring-(& [color:var(--theme--accent-primary)])
|
||||||
|
${className ?? ""} ${type === "color"
|
||||||
|
? "bg-([image:repeating-linear-gradient(45deg,#aaa_25%,transparent_25%,transparent_75%,#aaa_75%,#aaa),repeating-linear-gradient(45deg,#aaa_25%,#fff_25%,#fff_75%,#aaa_75%,#aaa)] [position:0_0,4px_4px] [size:8px_8px])"
|
||||||
|
: "bg-[color:var(--theme--bg-hover)]"}"
|
||||||
|
>${$input}${$icon}
|
||||||
|
</label>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input };
|
68
src/core/menu/components/Popup.mjs
Normal file
68
src/core/menu/components/Popup.mjs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { setState, useState, extendProps } from "../state.mjs";
|
||||||
|
|
||||||
|
function Popup({ trigger, onopen, onclose, onbeforeclose }, ...children) {
|
||||||
|
const { html } = globalThis.__enhancerApi,
|
||||||
|
$popup = html`<div
|
||||||
|
class="notion-enhancer--menu-popup
|
||||||
|
group absolute top-0 left-0 w-full h-full
|
||||||
|
flex flex-col justify-center items-end
|
||||||
|
pointer-events-none z-20"
|
||||||
|
>
|
||||||
|
<div class="relative right-[100%]">
|
||||||
|
<div
|
||||||
|
class="bg-[color:var(--theme--bg-secondary)]
|
||||||
|
w-[250px] max-w-[calc(100vw-24px)] max-h-[70vh]
|
||||||
|
py-[6px] px-[4px] drop-shadow-xl overflow-y-auto
|
||||||
|
transition duration-[200ms] opacity-0 scale-95 rounded-[4px]
|
||||||
|
group-open:(pointer-events-auto opacity-100 scale-100)"
|
||||||
|
>
|
||||||
|
${children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
$popup.show = () => {
|
||||||
|
$popup.setAttribute("open", true);
|
||||||
|
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = 0));
|
||||||
|
setState({ popupOpen: true });
|
||||||
|
onopen?.();
|
||||||
|
};
|
||||||
|
$popup.hide = () => {
|
||||||
|
$popup.removeAttribute("open");
|
||||||
|
$popup.style.pointerEvents = "auto";
|
||||||
|
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
|
||||||
|
onbeforeclose?.();
|
||||||
|
setTimeout(() => {
|
||||||
|
$popup.style.pointerEvents = "";
|
||||||
|
setState({ popupOpen: false });
|
||||||
|
onclose?.();
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
$popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
|
||||||
|
|
||||||
|
extendProps(trigger, {
|
||||||
|
onclick: $popup.show,
|
||||||
|
onkeydown(event) {
|
||||||
|
if (event.key === "Enter") $popup.show();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
useState(["rerender"], () => {
|
||||||
|
if ($popup.hasAttribute("open")) $popup.hide();
|
||||||
|
});
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (!$popup.hasAttribute("open")) return;
|
||||||
|
if ($popup.contains(event.target) || $popup === event.target) return;
|
||||||
|
if (trigger.contains(event.target) || trigger === event.target) return;
|
||||||
|
$popup.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
return $popup;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popup };
|
76
src/core/menu/components/Select.mjs
Normal file
76
src/core/menu/components/Select.mjs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, extendProps } from "../state.mjs";
|
||||||
|
import { Popup } from "./Popup.mjs";
|
||||||
|
|
||||||
|
function Option({ value, _get, _set }) {
|
||||||
|
const { html } = globalThis.__enhancerApi,
|
||||||
|
$selected = html`<i class="ml-auto i-check w-[16px] h-[16px]"></i>`,
|
||||||
|
$option = html`<div
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
class="select-none cursor-pointer rounded-[3px]
|
||||||
|
flex items-center w-full h-[28px] px-[12px] leading-[1.2]
|
||||||
|
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
||||||
|
onclick=${() => _set?.(value)}
|
||||||
|
onkeydown=${(event) => {
|
||||||
|
if (event.key === "Enter") _set?.(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="mr-[6px] text-[14px] text-ellipsis overflow-hidden">
|
||||||
|
${value}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
useState(["rerender"], () => {
|
||||||
|
_get?.().then((actualValue) => {
|
||||||
|
if (actualValue === value) {
|
||||||
|
$option.append($selected);
|
||||||
|
} else $selected.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return $option;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Select({ values, _get, _set, ...props }) {
|
||||||
|
const { html } = globalThis.__enhancerApi,
|
||||||
|
// dir="rtl" overflows to the left during transition
|
||||||
|
$select = html`<div
|
||||||
|
dir="rtl"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
class="appearance-none bg-transparent rounded-[4px] cursor-pointer
|
||||||
|
text-[14px] leading-[28px] h-[28px] max-w-[256px] pl-[8px] pr-[28px]
|
||||||
|
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
||||||
|
...${props}
|
||||||
|
></div>`;
|
||||||
|
useState(["rerender"], () => {
|
||||||
|
_get?.().then((value) => ($select.innerText = value));
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`<div class="notion-enhancer--menu-select relative">
|
||||||
|
${$select}
|
||||||
|
<${Popup}
|
||||||
|
trigger=${$select}
|
||||||
|
onbeforeclose=${() => {
|
||||||
|
$select.style.width = `${$select.offsetWidth}px`;
|
||||||
|
$select.style.background = "transparent";
|
||||||
|
}}
|
||||||
|
onclose=${() => {
|
||||||
|
$select.style.width = "";
|
||||||
|
$select.style.background = "";
|
||||||
|
}}
|
||||||
|
>${values.map((value) => html`<${Option} ...${{ value, _get, _set }} />`)}
|
||||||
|
<//>
|
||||||
|
<i
|
||||||
|
class="i-chevron-down pointer-events-none
|
||||||
|
absolute right-[6px] top-[6px] w-[16px] h-[16px]
|
||||||
|
text-[color:var(--theme--fg-secondary)]"
|
||||||
|
></i>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Select };
|
42
src/core/menu/components/Toggle.mjs
Normal file
42
src/core/menu/components/Toggle.mjs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, extendProps } from "../state.mjs";
|
||||||
|
|
||||||
|
function Toggle({ _get, _set, ...props }) {
|
||||||
|
const { html } = globalThis.__enhancerApi,
|
||||||
|
$input = html`<input
|
||||||
|
type="checkbox"
|
||||||
|
class="hidden checked:sibling:children:(
|
||||||
|
bg-[color:var(--theme--accent-primary)] after:translate-x-[12px])"
|
||||||
|
...${props}
|
||||||
|
/>`;
|
||||||
|
extendProps($input, { onchange: () => _set?.($input.checked) });
|
||||||
|
useState(["rerender"], () => {
|
||||||
|
_get?.().then((checked) => ($input.checked = checked));
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`<div class="notion-enhancer--menu-toggle shrink-0">
|
||||||
|
${$input}
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
class="w-[30px] h-[18px] rounded-[44px] cursor-pointer
|
||||||
|
transition duration-200 bg-[color:var(--theme--bg-hover)]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full h-full rounded-[44px] text-[12px]
|
||||||
|
p-[2px] hover:bg-[color:var(--theme--bg-hover)]
|
||||||
|
transition duration-200 after:(
|
||||||
|
inline-block w-[14px] h-[14px] rounded-[44px]
|
||||||
|
bg-[color:var(--theme--accent-primary\\_contrast)]
|
||||||
|
transition duration-200
|
||||||
|
)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toggle };
|
59
src/core/menu/islands/Footer.mjs
Normal file
59
src/core/menu/islands/Footer.mjs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { setState, useState } from "../state.mjs";
|
||||||
|
import { Button } from "../components/Button.mjs";
|
||||||
|
|
||||||
|
function Footer({ categories }) {
|
||||||
|
const { html, reloadApp } = globalThis.__enhancerApi,
|
||||||
|
$reload = html`<${Button}
|
||||||
|
class="ml-auto"
|
||||||
|
variant="primary"
|
||||||
|
icon="refresh-cw"
|
||||||
|
onclick=${reloadApp}
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
Reload & Apply Changes
|
||||||
|
<//>`,
|
||||||
|
$categories = categories.map(({ id, title, mods }) => {
|
||||||
|
return [
|
||||||
|
mods.map((mod) => mod.id),
|
||||||
|
html`<${Button}
|
||||||
|
icon="chevron-left"
|
||||||
|
onclick=${() => setState({ transition: "slide-to-left", view: id })}
|
||||||
|
>
|
||||||
|
${title}
|
||||||
|
<//>`,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons = [...$categories.map(([, $btn]) => $btn), $reload],
|
||||||
|
updateFooter = () => {
|
||||||
|
const buttonsVisible = buttons.some(($el) => $el.style.display === "");
|
||||||
|
setState({ footerOpen: buttonsVisible });
|
||||||
|
};
|
||||||
|
useState(["view"], ([view]) => {
|
||||||
|
for (const [ids, $btn] of $categories) {
|
||||||
|
const modActive = ids.some((id) => id === view);
|
||||||
|
$btn.style.display = modActive ? "" : "none";
|
||||||
|
}
|
||||||
|
updateFooter();
|
||||||
|
});
|
||||||
|
useState(["databaseUpdated"], ([databaseUpdated]) => {
|
||||||
|
$reload.style.display = databaseUpdated ? "" : "none";
|
||||||
|
updateFooter();
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`<div
|
||||||
|
class="flex w-full px-[60px] py-[16px]
|
||||||
|
border-t-(& [color:var(--theme--fg-border)])
|
||||||
|
bg-[color:var(--theme--bg-primary)]"
|
||||||
|
>
|
||||||
|
${buttons}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Footer };
|
57
src/core/menu/islands/List.mjs
Normal file
57
src/core/menu/islands/List.mjs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { setState } from "../state.mjs";
|
||||||
|
import { Description } from "../components/Description.mjs";
|
||||||
|
import { Input } from "../components/Input.mjs";
|
||||||
|
import { Mod } from "./Mod.mjs";
|
||||||
|
|
||||||
|
function Search({ items, itemType }) {
|
||||||
|
const { html, addKeyListener } = globalThis.__enhancerApi,
|
||||||
|
$search = html`<${Input}
|
||||||
|
type="text"
|
||||||
|
icon="search"
|
||||||
|
variant="lg"
|
||||||
|
placeholder="Search ${items.length} ${items.length === 1
|
||||||
|
? itemType.replace(/s$/, "")
|
||||||
|
: itemType} (Press '/' to focus)"
|
||||||
|
oninput=${(event) => {
|
||||||
|
const query = event.target.value.toLowerCase();
|
||||||
|
for (const $item of items) {
|
||||||
|
const matches = $item.innerText.toLowerCase().includes(query);
|
||||||
|
$item.style.display = matches ? "" : "none";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>`;
|
||||||
|
addKeyListener("/", (event) => {
|
||||||
|
if (document.activeElement?.nodeName === "INPUT") return;
|
||||||
|
// offsetParent == null if parent has "display: none;"
|
||||||
|
if ($search.offsetParent) {
|
||||||
|
event.preventDefault();
|
||||||
|
$search.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return $search;
|
||||||
|
}
|
||||||
|
|
||||||
|
function List({ id, mods, description }) {
|
||||||
|
const { html, isEnabled, setEnabled } = globalThis.__enhancerApi,
|
||||||
|
$mods = mods.map((mod) => {
|
||||||
|
const _get = () => isEnabled(mod.id),
|
||||||
|
_set = async (enabled) => {
|
||||||
|
await setEnabled(mod.id, enabled);
|
||||||
|
setState({ rerender: true, databaseUpdated: true });
|
||||||
|
};
|
||||||
|
return html`<${Mod} ...${{ ...mod, _get, _set }} />`;
|
||||||
|
});
|
||||||
|
return html`<div class="flex flex-col gap-y-[14px]">
|
||||||
|
<${Search} items=${$mods} itemType=${id} />
|
||||||
|
<${Description} innerHTML=${description} />
|
||||||
|
${$mods}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { List };
|
85
src/core/menu/islands/Mod.mjs
Normal file
85
src/core/menu/islands/Mod.mjs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { setState } from "../state.mjs";
|
||||||
|
import { Description } from "../components/Description.mjs";
|
||||||
|
import { Toggle } from "../components/Toggle.mjs";
|
||||||
|
|
||||||
|
function Mod({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
description,
|
||||||
|
thumbnail,
|
||||||
|
tags = [],
|
||||||
|
authors,
|
||||||
|
options = [],
|
||||||
|
_get,
|
||||||
|
_set,
|
||||||
|
_src,
|
||||||
|
}) {
|
||||||
|
const { html, enhancerUrl } = globalThis.__enhancerApi,
|
||||||
|
toggleId = Math.random().toString(36).slice(2, 5);
|
||||||
|
|
||||||
|
return html`<label
|
||||||
|
for=${toggleId}
|
||||||
|
class="notion-enhancer--menu-mod flex items-stretch rounded-[4px]
|
||||||
|
bg-[color:var(--theme--bg-secondary)] w-full py-[18px] px-[16px]
|
||||||
|
border border-[color:var(--theme--fg-border)] cursor-pointer
|
||||||
|
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
||||||
|
>
|
||||||
|
${thumbnail
|
||||||
|
? html`<img
|
||||||
|
src="${enhancerUrl(`${_src}/${thumbnail}`)}"
|
||||||
|
class="rounded-[4px] mr-[12px] h-[74px] my-auto"
|
||||||
|
/>`
|
||||||
|
: ""}
|
||||||
|
<div class="flex flex-col max-w-[50%]">
|
||||||
|
<div class="flex items-center text-[14px] mb-[5px]">
|
||||||
|
<h3 class="my-0">${name}</h3>
|
||||||
|
${[`v${version}`, ...tags].map((tag) => {
|
||||||
|
return html`<span
|
||||||
|
class="text-([12px] [color:var(--theme--fg-secondary)])
|
||||||
|
ml-[8px] py-[2px] px-[6px] leading-tight tracking-wide
|
||||||
|
rounded-[3px] bg-[color:var(--theme--bg-hover)]"
|
||||||
|
>
|
||||||
|
${tag}
|
||||||
|
</span>`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<${Description} class="mb-[6px]" innerHTML=${description} />
|
||||||
|
<div class="mt-auto flex gap-x-[8px] text-[12px] leading-[16px]">
|
||||||
|
${authors.map((author) => {
|
||||||
|
return html`<a href=${author.homepage} class="flex items-center">
|
||||||
|
<img src=${author.avatar} alt="" class="h-[12px] rounded-full" />
|
||||||
|
<span class="ml-[6px]">${author.name}</span>
|
||||||
|
</a>`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex ml-auto">
|
||||||
|
${options.length
|
||||||
|
? html`<button
|
||||||
|
class="flex items-center p-[4px] rounded-[4px] transition
|
||||||
|
text-[color:var(--theme--fg-secondary)] my-auto mr-[8px]
|
||||||
|
duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]
|
||||||
|
active:text-[color:var(--theme--fg-primary)]"
|
||||||
|
onclick=${() => {
|
||||||
|
setState({ transition: "slide-to-right", view: id });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i class="i-settings w-[18px] h-[18px]"></i>
|
||||||
|
</button>`
|
||||||
|
: ""}
|
||||||
|
<div class="my-auto scale-[1.15]">
|
||||||
|
<${Toggle} id=${toggleId} ...${{ _get, _set }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Mod };
|
86
src/core/menu/islands/Options.mjs
Normal file
86
src/core/menu/islands/Options.mjs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { setState } from "../state.mjs";
|
||||||
|
import { Heading } from "../components/Heading.mjs";
|
||||||
|
import { Description } from "../components/Description.mjs";
|
||||||
|
import { Input } from "../components/Input.mjs";
|
||||||
|
import { Select } from "../components/Select.mjs";
|
||||||
|
import { Toggle } from "../components/Toggle.mjs";
|
||||||
|
|
||||||
|
const camelToSentenceCase = (string) =>
|
||||||
|
string[0].toUpperCase() +
|
||||||
|
string.replace(/[A-Z]/g, (match) => ` ${match.toLowerCase()}`).slice(1),
|
||||||
|
filterOptionsForRender = (options) => {
|
||||||
|
const { platform } = globalThis.__enhancerApi;
|
||||||
|
options = options.reduce((options, opt) => {
|
||||||
|
// option must have key, headings may use label
|
||||||
|
if (!opt.key && (opt.type !== "heading" || !opt.label)) return options;
|
||||||
|
// ignore platform-specific options
|
||||||
|
if (opt.platforms && !opt.platforms.includes(platform)) return options;
|
||||||
|
// replace consective headings
|
||||||
|
const prev = options[options.length - 1];
|
||||||
|
if (opt.type === "heading" && prev?.type === opt.type) {
|
||||||
|
options[options.length - 1] = opt;
|
||||||
|
} else options.push(opt);
|
||||||
|
return options;
|
||||||
|
}, []);
|
||||||
|
// remove trailing heading
|
||||||
|
return options.at(-1)?.type === "heading" ? options.slice(0, -1) : options;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Option({ _get, _set, ...opt }) {
|
||||||
|
const { html } = globalThis.__enhancerApi;
|
||||||
|
return html`<${opt.type === "toggle" ? "label" : "div"}
|
||||||
|
class="notion-enhancer--menu-option flex items-center justify-between
|
||||||
|
mb-[18px] ${opt.type === "toggle" ? "cursor-pointer" : ""}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col ${opt.type === "text" ? "w-full" : "mr-[10%]"}">
|
||||||
|
<h5 class="text-[14px] mb-[2px] mt-0">${opt.label}</h5>
|
||||||
|
${opt.type === "text"
|
||||||
|
? html`<${Input}
|
||||||
|
type="text"
|
||||||
|
class="mt-[4px] mb-[8px]"
|
||||||
|
...${{ _get, _set }}
|
||||||
|
/>`
|
||||||
|
: ""}
|
||||||
|
<${Description} innerHTML=${opt.description} />
|
||||||
|
</div>
|
||||||
|
${["number", "hotkey", "color"].includes(opt.type)
|
||||||
|
? html`<${Input}
|
||||||
|
type=${opt.type}
|
||||||
|
class="shrink-0 !w-[192px]"
|
||||||
|
...${{ _get, _set }}
|
||||||
|
/>`
|
||||||
|
: opt.type === "file"
|
||||||
|
? html`<${Input}
|
||||||
|
type="file"
|
||||||
|
extensions=${opt.extensions}
|
||||||
|
...${{ _get, _set }}
|
||||||
|
/>`
|
||||||
|
: opt.type === "select"
|
||||||
|
? html`<${Select} values=${opt.values} ...${{ _get, _set }} />`
|
||||||
|
: opt.type === "toggle"
|
||||||
|
? html`<${Toggle} ...${{ _get, _set }} />`
|
||||||
|
: ""}
|
||||||
|
<//>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Options({ mod }) {
|
||||||
|
const { html, modDatabase } = globalThis.__enhancerApi;
|
||||||
|
return filterOptionsForRender(mod.options).map((opt) => {
|
||||||
|
opt.label ??= camelToSentenceCase(opt.key);
|
||||||
|
if (opt.type === "heading") return html`<${Heading}>${opt.label}<//>`;
|
||||||
|
const _get = async () => (await modDatabase(mod.id)).get(opt.key),
|
||||||
|
_set = async (value) => {
|
||||||
|
await (await modDatabase(mod.id)).set(opt.key, value);
|
||||||
|
setState({ rerender: true, databaseUpdated: true });
|
||||||
|
};
|
||||||
|
return html`<${Option} ...${{ _get, _set, ...opt }} />`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Options };
|
204
src/core/menu/islands/Profiles.mjs
Normal file
204
src/core/menu/islands/Profiles.mjs
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { setState, useState } from "../state.mjs";
|
||||||
|
import { Heading } from "../components/Heading.mjs";
|
||||||
|
import { Description } from "../components/Description.mjs";
|
||||||
|
import { Checkbox } from "../components/Checkbox.mjs";
|
||||||
|
import { Button } from "../components/Button.mjs";
|
||||||
|
import { Input } from "../components/Input.mjs";
|
||||||
|
import { Popup } from "../components/Popup.mjs";
|
||||||
|
|
||||||
|
function Profile({ id }) {
|
||||||
|
const { html, getProfile, initDatabase } = globalThis.__enhancerApi,
|
||||||
|
profile = initDatabase([id]),
|
||||||
|
db = initDatabase();
|
||||||
|
|
||||||
|
const getName = async () => {
|
||||||
|
let profileName = await profile.get("profileName");
|
||||||
|
if (id === "default") profileName ??= "default";
|
||||||
|
return profileName ?? "";
|
||||||
|
},
|
||||||
|
setName = async (name) => {
|
||||||
|
// name only has effect in menu
|
||||||
|
// doesn't need reload triggered
|
||||||
|
await profile.set("profileName", name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isActive = async () => id === (await getProfile()),
|
||||||
|
setActive = async () => {
|
||||||
|
await db.set("activeProfile", id);
|
||||||
|
setState({ rerender: true, databaseUpdated: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadProfile = (event) => {
|
||||||
|
const file = event.target.files[0],
|
||||||
|
reader = new FileReader();
|
||||||
|
reader.onload = async (progress) => {
|
||||||
|
const res = progress.currentTarget.result;
|
||||||
|
try {
|
||||||
|
await profile.import({
|
||||||
|
...JSON.parse(res),
|
||||||
|
profileName: await getName(),
|
||||||
|
});
|
||||||
|
setState({ rerender: true, databaseUpdated: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
},
|
||||||
|
downloadProfile = async () => {
|
||||||
|
const now = new Date(),
|
||||||
|
year = now.getFullYear().toString(),
|
||||||
|
month = (now.getMonth() + 1).toString().padStart(2, "0"),
|
||||||
|
day = now.getDate().toString().padStart(2, "0"),
|
||||||
|
hour = now.getHours().toString().padStart(2, "0"),
|
||||||
|
min = now.getMinutes().toString().padStart(2, "0"),
|
||||||
|
sec = now.getSeconds().toString().padStart(2, "0"),
|
||||||
|
date = year + month + day + hour + min + sec;
|
||||||
|
const $a = html`<a
|
||||||
|
class="hidden"
|
||||||
|
download="notion-enhancer_${await getName()}_${date}.json"
|
||||||
|
href="data:text/json;charset=utf-8,${encodeURIComponent(
|
||||||
|
JSON.stringify(await profile.export())
|
||||||
|
)}"
|
||||||
|
/>`;
|
||||||
|
document.body.append($a);
|
||||||
|
$a.click();
|
||||||
|
$a.remove();
|
||||||
|
},
|
||||||
|
deleteProfile = async () => {
|
||||||
|
let profileIds = await db.get("profileIds");
|
||||||
|
if (!profileIds?.length) profileIds = ["default"];
|
||||||
|
// clear profile data
|
||||||
|
const keys = Object.keys(await profile.export());
|
||||||
|
await profile.remove(keys);
|
||||||
|
// remove profile from list
|
||||||
|
const index = profileIds.indexOf(id);
|
||||||
|
if (index > -1) profileIds.splice(index, 1);
|
||||||
|
await db.set("profileIds", profileIds);
|
||||||
|
if (await isActive()) {
|
||||||
|
await db.remove("activeProfile");
|
||||||
|
setState({ rerender: true, databaseUpdated: true });
|
||||||
|
} else setState({ rerender: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const $name = html`<span
|
||||||
|
class="py-[2px] px-[4px] rounded-[3px]
|
||||||
|
bg-[color:var(--theme--bg-hover)]"
|
||||||
|
></span>`,
|
||||||
|
$delete = html`<button
|
||||||
|
class="h-[14px] transition duration-[20ms]
|
||||||
|
text-[color:var(--theme--fg-secondary)]
|
||||||
|
hover:text-[color:var(--theme--fg-primary)]"
|
||||||
|
>
|
||||||
|
<i class="i-x w-[14px] h-[14px]"></i>
|
||||||
|
</button>`,
|
||||||
|
$confirm = html`<${Popup}
|
||||||
|
trigger=${$delete}
|
||||||
|
onopen=${async () => ($name.innerText = await getName())}
|
||||||
|
>
|
||||||
|
<p class="text-[14px] pt-[2px] px-[8px]">
|
||||||
|
Are you sure you want to delete the profile ${$name} permanently?
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col gap-[8px] p-[8px]">
|
||||||
|
<${Button}
|
||||||
|
tabindex="0"
|
||||||
|
icon="trash"
|
||||||
|
class="justify-center"
|
||||||
|
variant="secondary"
|
||||||
|
onclick=${deleteProfile}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
<//>
|
||||||
|
<${Button}
|
||||||
|
tabindex="0"
|
||||||
|
class="justify-center"
|
||||||
|
onclick=${() => $confirm.close()}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
<//>
|
||||||
|
</div>
|
||||||
|
<//>`;
|
||||||
|
|
||||||
|
return html`<li class="flex items-center my-[14px] gap-[8px]" id=${id}>
|
||||||
|
<${Checkbox}
|
||||||
|
...${{ _get: isActive, _set: setActive }}
|
||||||
|
onchange=${(event) => (event.target.checked = true)}
|
||||||
|
/>
|
||||||
|
<${Input} icon="file-cog" ...${{ _get: getName, _set: setName }} />
|
||||||
|
<${Button} size="sm" icon="import" tagName="label">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="hidden"
|
||||||
|
accept=".json"
|
||||||
|
onchange=${uploadProfile}
|
||||||
|
/>
|
||||||
|
Import
|
||||||
|
<//>
|
||||||
|
<${Button} size="sm" icon="upload" onclick=${downloadProfile}>Export<//>
|
||||||
|
<div class="relative flex">${$delete}${$confirm}</div>
|
||||||
|
</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Profiles() {
|
||||||
|
const { html, initDatabase } = globalThis.__enhancerApi,
|
||||||
|
$input = html`<${Input} icon="file-cog" />`,
|
||||||
|
$list = html`<ul></ul>`;
|
||||||
|
|
||||||
|
const db = initDatabase(),
|
||||||
|
refreshProfiles = async () => {
|
||||||
|
let profileIds = await db.get("profileIds");
|
||||||
|
if (!profileIds?.length) profileIds = ["default"];
|
||||||
|
const $profiles = profileIds.map((id) => {
|
||||||
|
return document.getElementById(id) || html`<${Profile} id=${id} />`;
|
||||||
|
});
|
||||||
|
// replace rows one-by-one to avoid layout shift
|
||||||
|
for (let i = 0; i < $profiles.length || i < $list.children.length; i++) {
|
||||||
|
if ($profiles[i] === $list.children[i]) continue;
|
||||||
|
if ($list.children[i]) {
|
||||||
|
if ($profiles[i]) {
|
||||||
|
$list.children[i].replaceWith($profiles[i]);
|
||||||
|
} else $list.children[i].remove();
|
||||||
|
} else $list.append($profiles[i]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addProfile = async () => {
|
||||||
|
if (!$input.children[0].value) return;
|
||||||
|
const name = $input.children[0].value,
|
||||||
|
id = crypto.randomUUID();
|
||||||
|
let profileIds = await db.get("profileIds");
|
||||||
|
if (!profileIds?.length) profileIds = ["default"];
|
||||||
|
await db.set("profileIds", [...profileIds, id]);
|
||||||
|
await initDatabase([id]).set("profileName", name);
|
||||||
|
$input.children[0].value = "";
|
||||||
|
setState({ rerender: true });
|
||||||
|
};
|
||||||
|
useState(["rerender"], () => refreshProfiles());
|
||||||
|
$input.onkeydown = (event) => {
|
||||||
|
if (event.key === "Enter") addProfile();
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<${Heading}>Profiles<//>
|
||||||
|
<${Description}>
|
||||||
|
Profiles can be used to preserve and switch between notion-enhancer
|
||||||
|
configurations.
|
||||||
|
<//>
|
||||||
|
<div>
|
||||||
|
${$list}
|
||||||
|
<div class="flex items-center my-[14px] gap-[8px]">
|
||||||
|
${$input}
|
||||||
|
<${Button} variant="sm" icon="plus" onclick=${addProfile}>
|
||||||
|
Add Profile
|
||||||
|
<//>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Profiles };
|
88
src/core/menu/islands/Sidebar.mjs
Normal file
88
src/core/menu/islands/Sidebar.mjs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extendProps, setState, useState } from "../state.mjs";
|
||||||
|
|
||||||
|
function SidebarHeading({}, ...children) {
|
||||||
|
const { html } = globalThis.__enhancerApi;
|
||||||
|
return html`<h2
|
||||||
|
class="text-([11px] [color:var(--theme--fg-secondary)])
|
||||||
|
py-[5px] px-[15px] mb-px mt-[18px] first:mt-[10px]
|
||||||
|
uppercase font-medium tracking-[0.03em] leading-none"
|
||||||
|
>
|
||||||
|
${children}
|
||||||
|
</h2>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarButton({ id, icon, ...props }, ...children) {
|
||||||
|
const { html } = globalThis.__enhancerApi,
|
||||||
|
$btn = html`<${props.href ? "a" : "button"}
|
||||||
|
class="flex select-none cursor-pointer w-full
|
||||||
|
items-center py-[5px] px-[15px] text-[14px] last:mb-[12px]
|
||||||
|
transition hover:bg-[color:var(--theme--bg-hover)]"
|
||||||
|
...${props}
|
||||||
|
>
|
||||||
|
${icon
|
||||||
|
? html`<i
|
||||||
|
class="i-${icon} ${icon.startsWith("notion-enhancer")
|
||||||
|
? "w-[17px] h-[17px] ml-[1.5px] mr-[9.5px]"
|
||||||
|
: "w-[18px] h-[18px] ml-px mr-[9px]"}"
|
||||||
|
></i>`
|
||||||
|
: ""}
|
||||||
|
<span class="leading-[20px]">${children}</span>
|
||||||
|
<//>`;
|
||||||
|
|
||||||
|
if (!props.href) {
|
||||||
|
extendProps($btn, {
|
||||||
|
onclick: () => setState({ transition: "fade", view: id }),
|
||||||
|
});
|
||||||
|
useState(["view"], ([view = "welcome"]) => {
|
||||||
|
const active = view.toLowerCase() === id.toLowerCase();
|
||||||
|
$btn.style.background = active ? "var(--theme--bg-hover)" : "";
|
||||||
|
$btn.style.fontWeight = active ? "600" : "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return $btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sidebar({ items, categories }) {
|
||||||
|
const { html, isEnabled } = globalThis.__enhancerApi,
|
||||||
|
$sidebar = html`<aside
|
||||||
|
class="notion-enhancer--menu-sidebar z-10 row-span-1
|
||||||
|
h-full overflow-y-auto bg-[color:var(--theme--bg-secondary)]"
|
||||||
|
>
|
||||||
|
${items.map((item) => {
|
||||||
|
if (typeof item === "object") {
|
||||||
|
const { title, ...props } = item;
|
||||||
|
return html`<${SidebarButton} ...${props}>${title}<//>`;
|
||||||
|
} else return html`<${SidebarHeading}>${item}<//>`;
|
||||||
|
})}
|
||||||
|
</aside>`;
|
||||||
|
|
||||||
|
for (const { title, mods } of categories) {
|
||||||
|
const $title = html`<${SidebarHeading}>${title}<//>`,
|
||||||
|
$mods = mods.map((mod) => [
|
||||||
|
mod.id,
|
||||||
|
html`<${SidebarButton} id=${mod.id}>${mod.name}<//>`,
|
||||||
|
]);
|
||||||
|
$sidebar.append($title, ...$mods.map(([, $btn]) => $btn));
|
||||||
|
|
||||||
|
useState(["rerender"], async () => {
|
||||||
|
let sectionVisible = false;
|
||||||
|
for (const [id, $btn] of $mods) {
|
||||||
|
if (await isEnabled(id)) {
|
||||||
|
$btn.style.display = "";
|
||||||
|
sectionVisible = true;
|
||||||
|
} else $btn.style.display = "none";
|
||||||
|
}
|
||||||
|
$title.style.display = sectionVisible ? "" : "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sidebar;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Sidebar };
|
83
src/core/menu/islands/View.mjs
Normal file
83
src/core/menu/islands/View.mjs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* notion-enhancer
|
||||||
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from "../state.mjs";
|
||||||
|
|
||||||
|
function View({ id }, ...children) {
|
||||||
|
const { html } = globalThis.__enhancerApi,
|
||||||
|
$view = html`<article
|
||||||
|
id=${id}
|
||||||
|
class="notion-enhancer--menu-view h-full w-full
|
||||||
|
absolute overflow-y-auto px-[60px] py-[36px]"
|
||||||
|
>
|
||||||
|
${children}
|
||||||
|
</article>`;
|
||||||
|
|
||||||
|
useState(["view"], ([view = "welcome"]) => {
|
||||||
|
const [transition] = useState(["transition"]),
|
||||||
|
isVisible = $view.style.display !== "none",
|
||||||
|
nowActive = view.toLowerCase() === id.toLowerCase();
|
||||||
|
|
||||||
|
switch (transition) {
|
||||||
|
case "fade": {
|
||||||
|
const duration = 100,
|
||||||
|
cssTransition = `opacity ${duration}ms`;
|
||||||
|
if (isVisible && !nowActive) {
|
||||||
|
$view.style.transition = cssTransition;
|
||||||
|
$view.style.opacity = "0";
|
||||||
|
setTimeout(() => ($view.style.display = "none"), duration);
|
||||||
|
} else if (!isVisible && nowActive) {
|
||||||
|
setTimeout(() => {
|
||||||
|
$view.style.opacity = "0";
|
||||||
|
$view.style.display = "";
|
||||||
|
requestIdleCallback(() => {
|
||||||
|
$view.style.transition = cssTransition;
|
||||||
|
$view.style.opacity = "1";
|
||||||
|
});
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "slide-to-left":
|
||||||
|
case "slide-to-right": {
|
||||||
|
const duration = 200,
|
||||||
|
cssTransition = `opacity ${duration}ms, transform ${duration}ms`;
|
||||||
|
if (isVisible && !nowActive) {
|
||||||
|
$view.style.transition = cssTransition;
|
||||||
|
$view.style.transform = `translateX(${
|
||||||
|
transition === "slide-to-right" ? "-100%" : "100%"
|
||||||
|
})`;
|
||||||
|
$view.style.opacity = "0";
|
||||||
|
setTimeout(() => {
|
||||||
|
$view.style.display = "none";
|
||||||
|
$view.style.transform = "";
|
||||||
|
}, duration);
|
||||||
|
} else if (!isVisible && nowActive) {
|
||||||
|
$view.style.transform = `translateX(${
|
||||||
|
transition === "slide-to-right" ? "100%" : "-100%"
|
||||||
|
})`;
|
||||||
|
$view.style.opacity = "0";
|
||||||
|
$view.style.display = "";
|
||||||
|
requestIdleCallback(() => {
|
||||||
|
$view.style.transition = cssTransition;
|
||||||
|
$view.style.transform = "";
|
||||||
|
$view.style.opacity = "1";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
$view.style.transition = "";
|
||||||
|
$view.style.opacity = nowActive ? "1" : "0";
|
||||||
|
$view.style.display = nowActive ? "" : "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return $view;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { View };
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* notion-enhancer
|
* notion-enhancer
|
||||||
* (c) 2022 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (https://notion-enhancer.github.io/) under the MIT license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -94,9 +94,9 @@ body > #skeleton .row-group .shimmer {
|
|||||||
height: 11px;
|
height: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
mark {
|
.notion-enhancer--menu-description mark {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
padding: 2px 4px;
|
padding: 0 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background-color: var(--theme--bg-hover);
|
background-color: var(--theme--bg-hover);
|
||||||
}
|
}
|
||||||
|
@ -4,370 +4,144 @@
|
|||||||
* (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 { setState, useState } from "./state.mjs";
|
||||||
import {
|
import { Sidebar } from "./islands/Sidebar.mjs";
|
||||||
Button,
|
import { Footer } from "./islands/Footer.mjs";
|
||||||
Description,
|
import { View } from "./islands/View.mjs";
|
||||||
Sidebar,
|
import { List } from "./islands/List.mjs";
|
||||||
SidebarSection,
|
import { Mod } from "./islands/Mod.mjs";
|
||||||
SidebarButton,
|
import { Options } from "./islands/Options.mjs";
|
||||||
List,
|
import { Profiles } from "./islands/Profiles.mjs";
|
||||||
Footer,
|
|
||||||
View,
|
|
||||||
Input,
|
|
||||||
Mod,
|
|
||||||
Option,
|
|
||||||
Profile,
|
|
||||||
} from "./components.mjs";
|
|
||||||
|
|
||||||
const compatibleMods = (mods) => {
|
const categories = [
|
||||||
const { platform } = globalThis.__enhancerApi;
|
{
|
||||||
return mods.filter((mod) => {
|
icon: "palette",
|
||||||
const required =
|
id: "themes",
|
||||||
mod.id &&
|
title: "Themes",
|
||||||
mod.name &&
|
description: `Themes override Notion's colour schemes. Dark themes require
|
||||||
mod.version &&
|
|
||||||
mod.description &&
|
|
||||||
mod.thumbnail &&
|
|
||||||
mod.authors,
|
|
||||||
compatible = !mod.platforms || mod.platforms.includes(platform);
|
|
||||||
return required && compatible;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSidebar = (items, categories) => {
|
|
||||||
const { html, isEnabled } = globalThis.__enhancerApi,
|
|
||||||
$sidebar = html`<${Sidebar}>
|
|
||||||
${items.map((item) => {
|
|
||||||
if (typeof item === "object") {
|
|
||||||
const { title, ...props } = item;
|
|
||||||
return html`<${SidebarButton} ...${props}>${title}<//>`;
|
|
||||||
} else return html`<${SidebarSection}>${item}<//>`;
|
|
||||||
})}
|
|
||||||
<//>`;
|
|
||||||
for (const { title, mods } of categories) {
|
|
||||||
const $title = html`<${SidebarSection}>${title}<//>`,
|
|
||||||
$mods = mods.map((mod) => [
|
|
||||||
mod.id,
|
|
||||||
html`<${SidebarButton} id=${mod.id}>${mod.name}<//>`,
|
|
||||||
]);
|
|
||||||
$sidebar.append($title, ...$mods.map(([, $btn]) => $btn));
|
|
||||||
useState(["rerender"], async () => {
|
|
||||||
let sectionVisible = false;
|
|
||||||
for (const [id, $btn] of $mods) {
|
|
||||||
if (await isEnabled(id)) {
|
|
||||||
$btn.style.display = "";
|
|
||||||
sectionVisible = true;
|
|
||||||
} else $btn.style.display = "none";
|
|
||||||
}
|
|
||||||
$title.style.display = sectionVisible ? "" : "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return $sidebar;
|
|
||||||
},
|
|
||||||
renderList = async (id, mods, description) => {
|
|
||||||
const { html, isEnabled, setEnabled } = globalThis.__enhancerApi;
|
|
||||||
mods = mods.map(async (mod) => {
|
|
||||||
const _get = () => isEnabled(mod.id),
|
|
||||||
_set = async (enabled) => {
|
|
||||||
await setEnabled(mod.id, enabled);
|
|
||||||
setState({ rerender: true, databaseUpdated: true });
|
|
||||||
};
|
|
||||||
return html`<${Mod} ...${{ ...mod, _get, _set }} />`;
|
|
||||||
});
|
|
||||||
return html`<${List} ...${{ id, description }}>
|
|
||||||
${await Promise.all(mods)}
|
|
||||||
<//>`;
|
|
||||||
},
|
|
||||||
renderOptions = async (mod) => {
|
|
||||||
const { html, platform, modDatabase } = globalThis.__enhancerApi;
|
|
||||||
let options = mod.options.reduce((options, opt) => {
|
|
||||||
if (!opt.key && (opt.type !== "heading" || !opt.label)) return options;
|
|
||||||
if (opt.platforms && !opt.platforms.includes(platform)) return options;
|
|
||||||
const prevOpt = options[options.length - 1];
|
|
||||||
// no consective headings
|
|
||||||
if (opt.type === "heading" && prevOpt?.type === opt.type) {
|
|
||||||
options[options.length - 1] = opt;
|
|
||||||
} else options.push(opt);
|
|
||||||
return options;
|
|
||||||
}, []);
|
|
||||||
// no empty/end headings e.g. if section is platform-specific
|
|
||||||
if (options[options.length - 1]?.type === "heading") options.pop();
|
|
||||||
options = options.map(async (opt) => {
|
|
||||||
if (opt.type === "heading") return html`<${Option} ...${opt} />`;
|
|
||||||
const _get = async () => (await modDatabase(mod.id)).get(opt.key),
|
|
||||||
_set = async (value) => {
|
|
||||||
await (await modDatabase(mod.id)).set(opt.key, value);
|
|
||||||
setState({ rerender: true, databaseUpdated: true });
|
|
||||||
};
|
|
||||||
return html`<${Option} ...${{ ...opt, _get, _set }} />`;
|
|
||||||
});
|
|
||||||
return Promise.all(options);
|
|
||||||
},
|
|
||||||
renderProfiles = async () => {
|
|
||||||
let profileIds;
|
|
||||||
const { html, initDatabase, getProfile } = globalThis.__enhancerApi,
|
|
||||||
db = initDatabase(),
|
|
||||||
$list = html`<ul></ul>`,
|
|
||||||
renderProfile = (id) => {
|
|
||||||
const profile = initDatabase([id]),
|
|
||||||
isActive = async () => id === (await getProfile()),
|
|
||||||
deleteProfile = async () => {
|
|
||||||
const keys = Object.keys(await profile.export());
|
|
||||||
profileIds.splice(profileIds.indexOf(id), 1);
|
|
||||||
await db.set("profileIds", profileIds);
|
|
||||||
await profile.remove(keys);
|
|
||||||
if (isActive()) {
|
|
||||||
await db.remove("activeProfile");
|
|
||||||
setState({ databaseUpdated: true });
|
|
||||||
}
|
|
||||||
setState({ rerender: true });
|
|
||||||
};
|
|
||||||
return html`<${Profile}
|
|
||||||
id=${id}
|
|
||||||
getName=${async () =>
|
|
||||||
(await profile.get("profileName")) ??
|
|
||||||
(id === "default" ? "default" : "")}
|
|
||||||
setName=${(name) => profile.set("profileName", name)}
|
|
||||||
isActive=${isActive}
|
|
||||||
setActive=${async () => {
|
|
||||||
await db.set("activeProfile", id);
|
|
||||||
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 });
|
|
||||||
// success
|
|
||||||
} catch {
|
|
||||||
// error
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
deleteProfile=${deleteProfile}
|
|
||||||
/>`;
|
|
||||||
},
|
|
||||||
refreshProfiles = async () => {
|
|
||||||
profileIds = await db.get("profileIds");
|
|
||||||
if (!profileIds?.length) profileIds = ["default"];
|
|
||||||
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) => {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
await db.set("profileIds", [...profileIds, id]);
|
|
||||||
const profile = initDatabase([id]);
|
|
||||||
await profile.set("profileName", name);
|
|
||||||
refreshProfiles();
|
|
||||||
};
|
|
||||||
useState(["rerender"], () => refreshProfiles());
|
|
||||||
|
|
||||||
const $input = html`<${Input}
|
|
||||||
size="md"
|
|
||||||
type="text"
|
|
||||||
icon="file-cog"
|
|
||||||
onkeydown=${(event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
if (!$input.children[0].value) return;
|
|
||||||
addProfile($input.children[0].value);
|
|
||||||
$input.children[0].value = "";
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>`;
|
|
||||||
return html`<div>
|
|
||||||
${$list}
|
|
||||||
<div class="flex items-center my-[14px] gap-[8px]">
|
|
||||||
${$input}
|
|
||||||
<${Button}
|
|
||||||
size="sm"
|
|
||||||
icon="plus"
|
|
||||||
onclick=${() => {
|
|
||||||
if (!$input.children[0].value) return;
|
|
||||||
addProfile($input.children[0].value);
|
|
||||||
$input.children[0].value = "";
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add Profile
|
|
||||||
<//>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
},
|
|
||||||
renderMods = async (mods) => {
|
|
||||||
const { html, isEnabled, setEnabled } = globalThis.__enhancerApi;
|
|
||||||
mods = mods
|
|
||||||
.filter((mod) => {
|
|
||||||
return mod.options?.filter((opt) => opt.type !== "heading").length;
|
|
||||||
})
|
|
||||||
.map(async (mod) => {
|
|
||||||
const _get = () => isEnabled(mod.id),
|
|
||||||
_set = async (enabled) => {
|
|
||||||
await setEnabled(mod.id, enabled);
|
|
||||||
setState({ rerender: true, databaseUpdated: true });
|
|
||||||
};
|
|
||||||
return html`<${View} id=${mod.id}>
|
|
||||||
<${Mod} ...${{ ...mod, options: [], _get, _set }} />
|
|
||||||
${await renderOptions(mod)}
|
|
||||||
<//>`;
|
|
||||||
});
|
|
||||||
return Promise.all(mods);
|
|
||||||
};
|
|
||||||
|
|
||||||
const render = async () => {
|
|
||||||
const { html, reloadApp, getCore } = globalThis.__enhancerApi,
|
|
||||||
{ getThemes, getExtensions, getIntegrations } = globalThis.__enhancerApi,
|
|
||||||
[icon, renderStarted] = getState(["icon", "renderStarted"]);
|
|
||||||
if (!html || !getCore || !icon || renderStarted) return;
|
|
||||||
setState({ renderStarted: true });
|
|
||||||
|
|
||||||
const categories = [
|
|
||||||
{
|
|
||||||
icon: "palette",
|
|
||||||
id: "themes",
|
|
||||||
title: "Themes",
|
|
||||||
description: `Themes override Notion's colour schemes. Dark themes require
|
|
||||||
Notion to be in dark mode and light themes require Notion to be in light
|
Notion to be in dark mode and light themes require Notion to be in light
|
||||||
mode. To switch between dark mode and light mode, go to <mark>Settings &
|
mode. To switch between dark mode and light mode, go to <mark>Settings &
|
||||||
members → My notifications & settings → My settings → Appearance</mark>.`,
|
members → My notifications & settings → My settings → Appearance</mark>.`,
|
||||||
mods: compatibleMods(await getThemes()),
|
},
|
||||||
},
|
{
|
||||||
{
|
icon: "zap",
|
||||||
icon: "zap",
|
id: "extensions",
|
||||||
id: "extensions",
|
title: "Extensions",
|
||||||
title: "Extensions",
|
description: `Extensions add to the functionality and layout of the Notion
|
||||||
description: `Extensions add to the functionality and layout of the Notion
|
|
||||||
client, interacting with and modifying existing interfaces.`,
|
client, interacting with and modifying existing interfaces.`,
|
||||||
mods: compatibleMods(await getExtensions()),
|
},
|
||||||
},
|
{
|
||||||
{
|
icon: "plug",
|
||||||
icon: "plug",
|
id: "integrations",
|
||||||
id: "integrations",
|
title: "Integrations",
|
||||||
title: "Integrations",
|
description: `<span class="text-[color:var(--theme--fg-red)]">
|
||||||
description: `<span class="text-[color:var(--theme--fg-red)]">
|
|
||||||
Integrations access and modify Notion content. They interact directly with
|
Integrations access and modify Notion content. They interact directly with
|
||||||
<mark>https://www.notion.so/api/v3</mark>. Use at your own risk.</span>`,
|
<mark>https://www.notion.so/api/v3</mark>. Use at your own risk.</span>`,
|
||||||
mods: compatibleMods(await getIntegrations()),
|
},
|
||||||
},
|
],
|
||||||
],
|
sidebar = [
|
||||||
sidebar = [
|
"notion-enhancer",
|
||||||
"notion-enhancer",
|
{
|
||||||
{
|
id: "welcome",
|
||||||
id: "welcome",
|
title: "Welcome",
|
||||||
title: "Welcome",
|
icon: "notion-enhancer",
|
||||||
icon: `notion-enhancer${icon === "Monochrome" ? "?mask" : ""}`,
|
},
|
||||||
},
|
{
|
||||||
{
|
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",
|
{
|
||||||
{
|
id: "core",
|
||||||
id: "core",
|
title: "Core",
|
||||||
title: "Core",
|
icon: "sliders-horizontal",
|
||||||
icon: "sliders-horizontal",
|
},
|
||||||
},
|
...categories.map((c) => ({ id: c.id, title: c.title, icon: c.icon })),
|
||||||
...categories.map((c) => ({ id: c.id, title: c.title, icon: c.icon })),
|
];
|
||||||
];
|
|
||||||
|
|
||||||
// view wrapper necessary for transitions
|
const render = async () => {
|
||||||
const $views = html`<div class="grow relative overflow-hidden">
|
const { html, getMods, isEnabled, setEnabled } = globalThis.__enhancerApi,
|
||||||
<${View} id="welcome">welcome<//>
|
[icon, renderStarted] = useState(["icon", "renderStarted"]);
|
||||||
<${View} id="core">
|
if (!html || !getMods || !icon || renderStarted) return;
|
||||||
${await renderOptions(await getCore())}
|
if (icon === "Monochrome") sidebar[1].icon += "?mask";
|
||||||
<${Option} type="heading" label="Profiles" />
|
setState({ renderStarted: true });
|
||||||
<${Description}>
|
|
||||||
Profiles can be used to preserve and switch between notion-enhancer
|
const mods = await getMods();
|
||||||
configurations.
|
for (let i = 0; i < categories.length; i++) {
|
||||||
<//>
|
const { id } = categories[i];
|
||||||
${await renderProfiles()}
|
categories[i].mods = mods.filter(({ _src }) => _src.startsWith(`${id}/`));
|
||||||
<//>
|
categories[i].view = html`<${View} id=${id}>
|
||||||
</div>`;
|
<${List} ...${categories[i]} />
|
||||||
for (const { id, description, mods } of categories) {
|
<//>`;
|
||||||
const $list = await renderList(id, mods, description),
|
}
|
||||||
$mods = await renderMods(mods);
|
for (let i = 0; i < mods.length; i++) {
|
||||||
$views.append(html`<${View} id=${id}>${$list}<//>`, ...$mods);
|
const options = mods[i].options?.filter((opt) => opt.type !== "heading");
|
||||||
|
if (mods[i]._src === "core" || !options.length) continue;
|
||||||
|
const _get = () => isEnabled(mods[i].id),
|
||||||
|
_set = async (enabled) => {
|
||||||
|
await setEnabled(mods[i].id, enabled);
|
||||||
|
setState({ rerender: true, databaseUpdated: true });
|
||||||
|
};
|
||||||
|
mods[i].view = html`<${View} id=${mods[i].id}>
|
||||||
|
<!-- passing an empty options array hides the settings button -->
|
||||||
|
<${Mod} ...${{ ...mods[i], options: [], _get, _set }} />
|
||||||
|
<${Options} mod=${mods[i]} />
|
||||||
|
<//>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// footer appears only if buttons are visible
|
const $sidebar = html`<${Sidebar}
|
||||||
// - the matching category button appears on a mod's options page
|
items=${sidebar}
|
||||||
// - the reload button appears if any options are changed
|
categories=${categories}
|
||||||
categories.forEach((c) => {
|
/>`,
|
||||||
c.button = html`<${Button}
|
$main = html`
|
||||||
icon="chevron-left"
|
<main class="flex flex-col overflow-hidden transition-[height]">
|
||||||
onclick=${() => setState({ transition: "slide-to-left", view: c.id })}
|
<!-- wrapper necessary for transitions -->
|
||||||
>
|
<div class="grow relative overflow-hidden">
|
||||||
${c.title}
|
<${View} id="welcome">welcome<//>
|
||||||
<//>`;
|
<${View} id="core">
|
||||||
});
|
<${Options} mod=${mods.find(({ _src }) => _src === "core")} />
|
||||||
const $reload = html`<${Button}
|
<${Profiles} />
|
||||||
class="ml-auto"
|
<//>
|
||||||
variant="primary"
|
${[...categories, ...mods]
|
||||||
icon="refresh-cw"
|
.filter(({ view }) => view)
|
||||||
onclick=${() => reloadApp()}
|
.map(({ view }) => view)}
|
||||||
style="display: none"
|
</div>
|
||||||
>
|
<${Footer} categories=${categories} />
|
||||||
Reload & Apply Changes
|
</main>
|
||||||
<//>`,
|
`;
|
||||||
$footer = html`<${Footer}>${categories.map((c) => c.button)}${$reload}<//>`,
|
useState(["footerOpen"], ([footerOpen]) => {
|
||||||
$main = html`<div class="flex flex-col overflow-hidden transition-[height]">
|
$main.style.height = footerOpen ? "100%" : "calc(100% + 33px)";
|
||||||
${$views} ${$footer}
|
|
||||||
</div>`,
|
|
||||||
updateFooter = () => {
|
|
||||||
const buttons = [...$footer.children],
|
|
||||||
renderFooter = buttons.some(($el) => $el.style.display === "");
|
|
||||||
$main.style.height = renderFooter ? "100%" : "calc(100% + 33px)";
|
|
||||||
};
|
|
||||||
useState(["view"], ([view]) => {
|
|
||||||
for (const { mods, button } of categories) {
|
|
||||||
const renderButton = mods.some((mod) => mod.id === view);
|
|
||||||
button.style.display = renderButton ? "" : "none";
|
|
||||||
updateFooter();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
useState(["databaseUpdated"], ([databaseUpdated]) => {
|
|
||||||
if (!databaseUpdated) return;
|
|
||||||
$reload.style.display = "";
|
|
||||||
updateFooter();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const $skeleton = document.querySelector("#skeleton");
|
const $skeleton = document.querySelector("#skeleton");
|
||||||
$skeleton.replaceWith(renderSidebar(sidebar, categories), $main);
|
$skeleton.replaceWith($sidebar, $main);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("focus", () => setState({ rerender: true }));
|
window.addEventListener("focus", () => setState({ rerender: true }));
|
||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
if (event.data?.namespace !== "notion-enhancer") return;
|
if (event.data?.namespace !== "notion-enhancer") return;
|
||||||
const [hotkey, theme, icon] = getState(["hotkey", "theme", "icon"]);
|
const [hotkey, theme, icon] = useState(["hotkey", "theme", "icon"]);
|
||||||
setState({
|
setState({
|
||||||
rerender: true,
|
rerender: true,
|
||||||
hotkey: event.data?.hotkey ?? hotkey,
|
hotkey: event.data?.hotkey ?? hotkey,
|
||||||
@ -377,7 +151,7 @@ window.addEventListener("message", (event) => {
|
|||||||
});
|
});
|
||||||
useState(["hotkey"], ([hotkey]) => {
|
useState(["hotkey"], ([hotkey]) => {
|
||||||
const { addKeyListener } = globalThis.__enhancerApi ?? {},
|
const { addKeyListener } = globalThis.__enhancerApi ?? {},
|
||||||
[hotkeyRegistered] = getState(["hotkeyRegistered"]);
|
[hotkeyRegistered] = useState(["hotkeyRegistered"]);
|
||||||
if (!hotkey || !addKeyListener || hotkeyRegistered) return;
|
if (!hotkey || !addKeyListener || hotkeyRegistered) return;
|
||||||
setState({ hotkeyRegistered: true });
|
setState({ hotkeyRegistered: true });
|
||||||
addKeyListener(hotkey, (event) => {
|
addKeyListener(hotkey, (event) => {
|
||||||
@ -386,20 +160,20 @@ useState(["hotkey"], ([hotkey]) => {
|
|||||||
parent?.postMessage(msg, "*");
|
parent?.postMessage(msg, "*");
|
||||||
});
|
});
|
||||||
addKeyListener("Escape", () => {
|
addKeyListener("Escape", () => {
|
||||||
const [popupOpen] = getState(["popupOpen"]);
|
const [popupOpen] = useState(["popupOpen"]);
|
||||||
if (!popupOpen) {
|
if (!popupOpen) {
|
||||||
const msg = { namespace: "notion-enhancer", action: "close-menu" };
|
const msg = { namespace: "notion-enhancer", action: "close-menu" };
|
||||||
parent?.postMessage(msg, "*");
|
parent?.postMessage(msg, "*");
|
||||||
} else setState({ rerender: true });
|
} else setState({ rerender: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
useState(["theme"], ([theme]) => {
|
useState(["theme"], ([theme]) => {
|
||||||
if (theme === "dark") document.body.classList.add("dark");
|
if (theme === "dark") document.body.classList.add("dark");
|
||||||
if (theme === "light") document.body.classList.remove("dark");
|
if (theme === "light") document.body.classList.remove("dark");
|
||||||
});
|
});
|
||||||
|
|
||||||
useState(["rerender"], async () => {
|
useState(["rerender"], async () => {
|
||||||
const [theme, icon] = getState(["theme", "icon"]);
|
const [theme, icon] = useState(["theme", "icon"]);
|
||||||
if (!theme || !icon) return;
|
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
|
||||||
|
@ -5,11 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const _state = {},
|
const _state = {},
|
||||||
_subscribers = [],
|
_subscribers = [];
|
||||||
getState = (keys) => {
|
|
||||||
return keys.map((key) => _state[key]);
|
const setState = (state) => {
|
||||||
},
|
|
||||||
setState = (state) => {
|
|
||||||
Object.assign(_state, state);
|
Object.assign(_state, state);
|
||||||
const updates = Object.keys(state);
|
const updates = Object.keys(state);
|
||||||
_subscribers
|
_subscribers
|
||||||
@ -17,8 +15,27 @@ const _state = {},
|
|||||||
.forEach(([keys, callback]) => callback(keys.map((key) => _state[key])));
|
.forEach(([keys, callback]) => callback(keys.map((key) => _state[key])));
|
||||||
},
|
},
|
||||||
useState = (keys, callback) => {
|
useState = (keys, callback) => {
|
||||||
_subscribers.push([keys, callback]);
|
const state = keys.map((key) => _state[key]);
|
||||||
callback(getState(keys));
|
if (callback) _subscribers.push([keys, callback]);
|
||||||
|
callback?.(state);
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { setState, useState, getState };
|
const extendProps = (props, extend) => {
|
||||||
|
for (const key in extend) {
|
||||||
|
const { [key]: userProvided } = props;
|
||||||
|
if (typeof extend[key] === "function") {
|
||||||
|
props[key] = (...args) => {
|
||||||
|
extend[key](...args);
|
||||||
|
userProvided?.(...args);
|
||||||
|
};
|
||||||
|
} else if (key === "class") {
|
||||||
|
if (userProvided) props[key] += " ";
|
||||||
|
if (!userProvided) props[key] = "";
|
||||||
|
props[key] += extend[key];
|
||||||
|
} else props[key] = extend[key] ?? userProvided;
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { setState, useState, extendProps };
|
||||||
|
@ -1,292 +0,0 @@
|
|||||||
/**
|
|
||||||
* notion-enhancer: menu
|
|
||||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
|
||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import { fmt, web, registry, components } from '../../api/index.mjs';
|
|
||||||
import { notifications } from './notifications.mjs';
|
|
||||||
import '../../dep/jscolor.min.js';
|
|
||||||
|
|
||||||
import '../../dep/markdown-it.min.js';
|
|
||||||
const md = markdownit({ linkify: true });
|
|
||||||
|
|
||||||
export const modComponents = {
|
|
||||||
preview: (url) => web.html`<img
|
|
||||||
class="mod-preview"
|
|
||||||
src="${web.escape(url)}"
|
|
||||||
alt=""
|
|
||||||
>`,
|
|
||||||
title: (title) => web.html`<h4 class="mod-title"><span>${web.escape(title)}</span></h4>`,
|
|
||||||
version: (version) => web.html`<span class="mod-version">v${web.escape(version)}</span>`,
|
|
||||||
tags: (tags) => {
|
|
||||||
if (!tags.length) return '';
|
|
||||||
return web.render(
|
|
||||||
web.html`<p class="mod-tags"></p>`,
|
|
||||||
tags.map((tag) => `#${web.escape(tag)}`).join(' ')
|
|
||||||
);
|
|
||||||
},
|
|
||||||
description: (description) => {
|
|
||||||
const $description = web.html`<p class="mod-description markdown-inline">
|
|
||||||
${md.renderInline(description)}
|
|
||||||
</p>`;
|
|
||||||
$description.querySelectorAll('a').forEach((a) => {
|
|
||||||
a.target = '_blank';
|
|
||||||
});
|
|
||||||
return $description;
|
|
||||||
},
|
|
||||||
authors: (authors) => {
|
|
||||||
const author = (author) => web.html`<a
|
|
||||||
class="mod-author"
|
|
||||||
href="${web.escape(author.homepage)}"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<img class="mod-author-avatar"
|
|
||||||
src="${web.escape(author.avatar)}" alt="${web.escape(author.name)}'s avatar"
|
|
||||||
> <span>${web.escape(author.name)}</span>
|
|
||||||
</a>`;
|
|
||||||
return web.render(web.html`<p class="mod-authors-container"></p>`, ...authors.map(author));
|
|
||||||
},
|
|
||||||
toggle: (label, checked) => {
|
|
||||||
const $label = web.html`<label tabindex="0" class="toggle-label">
|
|
||||||
<span>${web.escape(label)}</span>
|
|
||||||
</label>`,
|
|
||||||
$input = web.html`<input tabindex="-1" type="checkbox" class="toggle-check"
|
|
||||||
${checked ? 'checked' : ''}>`,
|
|
||||||
$feature = web.html`<span class="toggle-box toggle-feature"></span>`;
|
|
||||||
$label.addEventListener('keyup', (event) => {
|
|
||||||
if (['Enter', ' '].includes(event.key)) $input.checked = !$input.checked;
|
|
||||||
});
|
|
||||||
return web.render($label, $input, $feature);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const options = {
|
|
||||||
toggle: async (mod, opt) => {
|
|
||||||
const profileDB = await registry.profileDB(),
|
|
||||||
checked = await profileDB.get([mod.id, opt.key], opt.value),
|
|
||||||
$toggle = modComponents.toggle(opt.label, checked),
|
|
||||||
$tooltipIcon = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = $toggle.children[0],
|
|
||||||
$input = $toggle.children[1];
|
|
||||||
if (opt.tooltip) {
|
|
||||||
$label.prepend($tooltipIcon);
|
|
||||||
components.addTooltip($tooltipIcon, opt.tooltip, {
|
|
||||||
offsetDirection: 'left',
|
|
||||||
maxLines: 3,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
$input.addEventListener('change', async (_event) => {
|
|
||||||
await profileDB.set([mod.id, opt.key], $input.checked);
|
|
||||||
notifications.onChange();
|
|
||||||
});
|
|
||||||
return $toggle;
|
|
||||||
},
|
|
||||||
|
|
||||||
select: async (mod, opt) => {
|
|
||||||
const profileDB = await registry.profileDB(),
|
|
||||||
value = await profileDB.get([mod.id, opt.key], opt.values[0]),
|
|
||||||
$tooltipIcon = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = web.render(
|
|
||||||
web.html`<label class="input-label"></label>`,
|
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltipIcon : '', opt.label)
|
|
||||||
),
|
|
||||||
$options = opt.values.map(
|
|
||||||
(option) => web.raw`<option
|
|
||||||
class="select-option"
|
|
||||||
value="${web.escape(option)}"
|
|
||||||
${option === value ? 'selected' : ''}
|
|
||||||
>${web.escape(option)}</option>`
|
|
||||||
),
|
|
||||||
$select = web.html`<select class="input">
|
|
||||||
${$options.join('')}
|
|
||||||
</select>`,
|
|
||||||
$icon = web.html`${await components.feather('chevron-down', { class: 'input-icon' })}`;
|
|
||||||
if (opt.tooltip)
|
|
||||||
components.addTooltip($tooltipIcon, opt.tooltip, {
|
|
||||||
offsetDirection: 'left',
|
|
||||||
maxLines: 3,
|
|
||||||
});
|
|
||||||
$select.addEventListener('change', async (_event) => {
|
|
||||||
await profileDB.set([mod.id, opt.key], $select.value);
|
|
||||||
notifications.onChange();
|
|
||||||
});
|
|
||||||
return web.render($label, $select, $icon);
|
|
||||||
},
|
|
||||||
|
|
||||||
text: async (mod, opt) => {
|
|
||||||
const profileDB = await registry.profileDB(),
|
|
||||||
value = await profileDB.get([mod.id, opt.key], opt.value),
|
|
||||||
$tooltipIcon = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = web.render(
|
|
||||||
web.html`<label class="input-label"></label>`,
|
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltipIcon : '', opt.label)
|
|
||||||
),
|
|
||||||
$input = web.html`<input type="text" class="input" value="${web.escape(value)}">`,
|
|
||||||
$icon = web.html`${await components.feather('type', { class: 'input-icon' })}`;
|
|
||||||
if (opt.tooltip)
|
|
||||||
components.addTooltip($tooltipIcon, opt.tooltip, {
|
|
||||||
offsetDirection: 'left',
|
|
||||||
maxLines: 3,
|
|
||||||
});
|
|
||||||
$input.addEventListener('change', async (_event) => {
|
|
||||||
await profileDB.set([mod.id, opt.key], $input.value);
|
|
||||||
notifications.onChange();
|
|
||||||
});
|
|
||||||
return web.render($label, $input, $icon);
|
|
||||||
},
|
|
||||||
|
|
||||||
number: async (mod, opt) => {
|
|
||||||
const profileDB = await registry.profileDB(),
|
|
||||||
value = await profileDB.get([mod.id, opt.key], opt.value),
|
|
||||||
$tooltipIcon = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = web.render(
|
|
||||||
web.html`<label class="input-label"></label>`,
|
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltipIcon : '', opt.label)
|
|
||||||
),
|
|
||||||
$input = web.html`<input type="number" class="input" value="${value}">`,
|
|
||||||
$icon = web.html`${await components.feather('hash', { class: 'input-icon' })}`;
|
|
||||||
if (opt.tooltip)
|
|
||||||
components.addTooltip($tooltipIcon, opt.tooltip, {
|
|
||||||
offsetDirection: 'left',
|
|
||||||
maxLines: 3,
|
|
||||||
});
|
|
||||||
$input.addEventListener('change', async (_event) => {
|
|
||||||
await profileDB.set([mod.id, opt.key], $input.value);
|
|
||||||
notifications.onChange();
|
|
||||||
});
|
|
||||||
return web.render($label, $input, $icon);
|
|
||||||
},
|
|
||||||
|
|
||||||
color: async (mod, opt) => {
|
|
||||||
const profileDB = await registry.profileDB(),
|
|
||||||
value = await profileDB.get([mod.id, opt.key], opt.value),
|
|
||||||
$tooltipIcon = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = web.render(
|
|
||||||
web.html`<label class="input-label"></label>`,
|
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltipIcon : '', opt.label)
|
|
||||||
),
|
|
||||||
$input = web.html`<input type="text" class="input">`,
|
|
||||||
$icon = web.html`${await components.feather('droplet', { class: 'input-icon' })}`,
|
|
||||||
paint = () => {
|
|
||||||
$input.style.background = $picker.toBackground();
|
|
||||||
const [r, g, b] = $picker
|
|
||||||
.toRGBAString()
|
|
||||||
.slice(5, -1)
|
|
||||||
.split(',')
|
|
||||||
.map((i) => parseInt(i));
|
|
||||||
$input.style.color = fmt.rgbContrast(r, g, b);
|
|
||||||
$input.style.padding = '';
|
|
||||||
},
|
|
||||||
$picker = new JSColor($input, {
|
|
||||||
value,
|
|
||||||
format: 'rgba',
|
|
||||||
previewSize: 0,
|
|
||||||
borderRadius: 3,
|
|
||||||
borderColor: 'var(--theme--ui_divider)',
|
|
||||||
controlBorderColor: 'var(--theme--ui_divider)',
|
|
||||||
backgroundColor: 'var(--theme--bg)',
|
|
||||||
onInput: paint,
|
|
||||||
onChange: paint,
|
|
||||||
});
|
|
||||||
if (opt.tooltip)
|
|
||||||
components.addTooltip($tooltipIcon, opt.tooltip, {
|
|
||||||
offsetDirection: 'left',
|
|
||||||
maxLines: 3,
|
|
||||||
});
|
|
||||||
$input.addEventListener('change', async (_event) => {
|
|
||||||
await profileDB.set([mod.id, opt.key], $input.value);
|
|
||||||
notifications.onChange();
|
|
||||||
});
|
|
||||||
paint();
|
|
||||||
return web.render($label, $input, $icon);
|
|
||||||
},
|
|
||||||
|
|
||||||
file: async (mod, opt) => {
|
|
||||||
const profileDB = await registry.profileDB(),
|
|
||||||
{ filename } = (await profileDB.get([mod.id, opt.key], {})) || {},
|
|
||||||
$tooltipIcon = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = web.render(
|
|
||||||
web.html`<label class="input-label"></label>`,
|
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltipIcon : '', opt.label)
|
|
||||||
),
|
|
||||||
$pseudo = web.html`<span class="input"><span class="input-placeholder">Upload file...</span></span>`,
|
|
||||||
$input = web.html`<input type="file" class="hidden" accept=${web.escape(
|
|
||||||
opt.extensions.join(',')
|
|
||||||
)}>`,
|
|
||||||
$icon = web.html`${await components.feather('file', { class: 'input-icon' })}`,
|
|
||||||
$filename = web.html`<span>${web.escape(filename || 'none')}</span>`,
|
|
||||||
$latest = web.render(web.html`<button class="file-latest">Latest: </button>`, $filename);
|
|
||||||
if (opt.tooltip)
|
|
||||||
components.addTooltip($tooltipIcon, opt.tooltip, {
|
|
||||||
offsetDirection: 'left',
|
|
||||||
maxLines: 3,
|
|
||||||
});
|
|
||||||
$input.addEventListener('change', (event) => {
|
|
||||||
const file = event.target.files[0],
|
|
||||||
reader = new FileReader();
|
|
||||||
reader.onload = async (progress) => {
|
|
||||||
$filename.innerText = file.name;
|
|
||||||
await profileDB.set([mod.id, opt.key], {
|
|
||||||
filename: file.name,
|
|
||||||
content: progress.currentTarget.result,
|
|
||||||
});
|
|
||||||
notifications.onChange();
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
$latest.addEventListener('click', (_event) => {
|
|
||||||
$filename.innerText = 'none';
|
|
||||||
profileDB.set([mod.id, opt.key], {});
|
|
||||||
});
|
|
||||||
return web.render(
|
|
||||||
web.html`<div></div>`,
|
|
||||||
web.render($label, $input, $pseudo, $icon),
|
|
||||||
$latest
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
hotkey: async (mod, opt) => {
|
|
||||||
const profileDB = await registry.profileDB(),
|
|
||||||
value = await profileDB.get([mod.id, opt.key], opt.value),
|
|
||||||
$tooltipIcon = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
|
|
||||||
$label = web.render(
|
|
||||||
web.html`<label class="input-label"></label>`,
|
|
||||||
web.render(web.html`<p></p>`, opt.tooltip ? $tooltipIcon : '', opt.label)
|
|
||||||
),
|
|
||||||
$input = web.html`<input type="text" class="input" value="${web.escape(value)}">`,
|
|
||||||
$icon = web.html`${await components.feather('command', { class: 'input-icon' })}`;
|
|
||||||
if (opt.tooltip)
|
|
||||||
components.addTooltip($tooltipIcon, opt.tooltip, {
|
|
||||||
offsetDirection: 'left',
|
|
||||||
maxLines: 3,
|
|
||||||
});
|
|
||||||
$input.addEventListener('keydown', async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const pressed = [],
|
|
||||||
modifiers = {
|
|
||||||
metaKey: 'Meta',
|
|
||||||
ctrlKey: 'Control',
|
|
||||||
altKey: 'Alt',
|
|
||||||
shiftKey: 'Shift',
|
|
||||||
};
|
|
||||||
for (const modifier in modifiers) {
|
|
||||||
if (event[modifier]) pressed.push(modifiers[modifier]);
|
|
||||||
}
|
|
||||||
const empty = ['Backspace', 'Delete'].includes(event.key) && !pressed.length;
|
|
||||||
if (!empty && !pressed.includes(event.key)) {
|
|
||||||
let key = event.key;
|
|
||||||
if (key === ' ') key = 'Space';
|
|
||||||
if (key === '+') key = 'Plus';
|
|
||||||
if (key.length === 1) key = event.key.toUpperCase();
|
|
||||||
pressed.push(key);
|
|
||||||
}
|
|
||||||
$input.value = pressed.join('+');
|
|
||||||
await profileDB.set([mod.id, opt.key], $input.value);
|
|
||||||
notifications.onChange();
|
|
||||||
});
|
|
||||||
return web.render($label, $input, $icon);
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "menu",
|
|
||||||
"id": "a6621988-551d-495a-97d8-3c568bca2e9e",
|
|
||||||
"version": "0.11.0",
|
|
||||||
"description": "the enhancer's graphical menu, related buttons and shortcuts.",
|
|
||||||
"tags": ["core"],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "dragonwocky",
|
|
||||||
"email": "thedragonring.bod@gmail.com",
|
|
||||||
"homepage": "https://dragonwocky.me/",
|
|
||||||
"avatar": "https://dragonwocky.me/avatar.jpg"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"css": {
|
|
||||||
"client": ["client.css"],
|
|
||||||
"menu": ["menu.css", "markdown.css"]
|
|
||||||
},
|
|
||||||
"js": {
|
|
||||||
"client": ["client.mjs"]
|
|
||||||
},
|
|
||||||
"options": [
|
|
||||||
{
|
|
||||||
"type": "hotkey",
|
|
||||||
"key": "hotkey",
|
|
||||||
"label": "toggle focus hotkey",
|
|
||||||
"tooltip": "**switches between notion & the enhancer menu**",
|
|
||||||
"value": "Ctrl+Alt+E"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -65,7 +65,7 @@
|
|||||||
"type": "color",
|
"type": "color",
|
||||||
"key": "color",
|
"key": "color",
|
||||||
"description": "Activates built-in debugging tools accessible through the application menu.",
|
"description": "Activates built-in debugging tools accessible through the application menu.",
|
||||||
"value": ""
|
"value": "rgb(232, 69, 93)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
13
src/init.js
13
src/init.js
@ -16,14 +16,8 @@ const isElectron = () => {
|
|||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
require("./api/electron.cjs");
|
require("./api/electron.cjs");
|
||||||
require("./api/mods.js");
|
require("./api/mods.js");
|
||||||
const {
|
const { enhancerUrl } = globalThis.__enhancerApi,
|
||||||
getMods,
|
{ getMods, isEnabled, modDatabase } = globalThis.__enhancerApi;
|
||||||
getProfile,
|
|
||||||
isEnabled,
|
|
||||||
optionDefaults,
|
|
||||||
enhancerUrl,
|
|
||||||
initDatabase,
|
|
||||||
} = globalThis.__enhancerApi;
|
|
||||||
|
|
||||||
module.exports = async (target, __exports, __eval) => {
|
module.exports = async (target, __exports, __eval) => {
|
||||||
if (target === "main/main") require("./worker.js");
|
if (target === "main/main") require("./worker.js");
|
||||||
@ -43,8 +37,7 @@ if (isElectron()) {
|
|||||||
// electronScripts
|
// electronScripts
|
||||||
for (const mod of await getMods()) {
|
for (const mod of await getMods()) {
|
||||||
if (!mod.electronScripts || !(await isEnabled(mod.id))) continue;
|
if (!mod.electronScripts || !(await isEnabled(mod.id))) continue;
|
||||||
const options = await optionDefaults(mod.id),
|
const db = await modDatabase(mod.id);
|
||||||
db = initDatabase([await getProfile(), mod.id], options);
|
|
||||||
for (const { source, target: targetScript } of mod.electronScripts) {
|
for (const { source, target: targetScript } of mod.electronScripts) {
|
||||||
if (`${target}.js` !== targetScript) continue;
|
if (`${target}.js` !== targetScript) continue;
|
||||||
const script = require(`notion-enhancer/${mod._src}/${source}`);
|
const script = require(`notion-enhancer/${mod._src}/${source}`);
|
||||||
|
22
src/load.mjs
22
src/load.mjs
@ -9,12 +9,10 @@
|
|||||||
export default (async () => {
|
export default (async () => {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const { enhancerUrl } = globalThis.__enhancerApi,
|
const { enhancerUrl } = globalThis.__enhancerApi,
|
||||||
isMenu = location.href.startsWith(enhancerUrl("/core/menu/index.html")),
|
isMenu = location.href.startsWith(enhancerUrl("/core/menu/index.html")),
|
||||||
pageLoaded = /(^\/$)|((-|\/)[0-9a-f]{32}((\?.+)|$))/.test(location.pathname),
|
pageLoaded = /(^\/$)|((-|\/)[0-9a-f]{32}((\?.+)|$))/.test(location.pathname),
|
||||||
signedIn = localStorage["LRU:KeyValueStore2:current-user-id"];
|
signedIn = localStorage["LRU:KeyValueStore2:current-user-id"];
|
||||||
if (!isMenu && !(signedIn && pageLoaded)) return;
|
if (!isMenu && (!signedIn || !pageLoaded)) return;
|
||||||
|
|
||||||
// avoid repeat logging
|
|
||||||
if (!isMenu) console.log("notion-enhancer: loading...");
|
if (!isMenu) console.log("notion-enhancer: loading...");
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@ -26,14 +24,12 @@ export default (async () => {
|
|||||||
import("./api/mods.js"),
|
import("./api/mods.js"),
|
||||||
]);
|
]);
|
||||||
await import("./api/interface.js");
|
await import("./api/interface.js");
|
||||||
const { getMods, getProfile } = globalThis.__enhancerApi,
|
const { getMods, isEnabled, modDatabase } = globalThis.__enhancerApi;
|
||||||
{ isEnabled, optionDefaults, initDatabase } = globalThis.__enhancerApi;
|
|
||||||
|
|
||||||
for (const mod of await getMods()) {
|
for (const mod of await getMods()) {
|
||||||
if (!(await isEnabled(mod.id))) continue;
|
if (!(await isEnabled(mod.id))) continue;
|
||||||
const isTheme = mod._src.startsWith("themes/"),
|
const isTheme = mod._src.startsWith("themes/");
|
||||||
isCore = mod._src === "core";
|
if (isMenu && !(mod._src === "core" || isTheme)) continue;
|
||||||
if (isMenu && !(isTheme || isCore)) continue;
|
|
||||||
|
|
||||||
// clientStyles
|
// clientStyles
|
||||||
for (let stylesheet of mod.clientStyles ?? []) {
|
for (let stylesheet of mod.clientStyles ?? []) {
|
||||||
@ -45,14 +41,12 @@ export default (async () => {
|
|||||||
|
|
||||||
// clientScripts
|
// clientScripts
|
||||||
if (isMenu) continue;
|
if (isMenu) continue;
|
||||||
const options = await optionDefaults(mod.id),
|
const db = await modDatabase(mod.id);
|
||||||
db = initDatabase([await getProfile(), mod.id], options);
|
|
||||||
for (let script of mod.clientScripts ?? []) {
|
for (let script of mod.clientScripts ?? []) {
|
||||||
script = await import(enhancerUrl(`${mod._src}/${script}`));
|
script = await import(enhancerUrl(`${mod._src}/${script}`));
|
||||||
script.default(globalThis.__enhancerApi, db);
|
script.default(globalThis.__enhancerApi, db);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// consider "ready" after menu has loaded
|
|
||||||
if (isMenu) console.log("notion-enhancer: ready");
|
if (isMenu) console.log("notion-enhancer: ready");
|
||||||
})();
|
})();
|
||||||
|
Loading…
Reference in New Issue
Block a user