mirror of
				https://github.com/notion-enhancer/notion-enhancer.git
				synced 2025-11-04 08:08:08 +11:00 
			
		
		
		
	feat(menu): indicate updates with popup & notification pings
This commit is contained in:
		
							parent
							
								
									0daf0a38c2
								
							
						
					
					
						commit
						3cd8ed7703
					
				@ -72,6 +72,7 @@ const encodeSvg = (svg) =>
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
twind.install({
 | 
			
		||||
  darkMode: "class",
 | 
			
		||||
  rules: [[/^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/, presetIcons]],
 | 
			
		||||
  variants: [
 | 
			
		||||
    // https://github.com/tw-in-js/twind/blob/main/packages/preset-ext/src/variants.ts
 | 
			
		||||
@ -558,5 +559,22 @@ const h = (type, props, ...children) => {
 | 
			
		||||
  },
 | 
			
		||||
  html = htm.bind(h);
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
globalThis.__enhancerApi ??= {};
 | 
			
		||||
Object.assign(globalThis.__enhancerApi, { html });
 | 
			
		||||
Object.assign(globalThis.__enhancerApi, { html, extendProps });
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,46 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { checkForUpdate } from "./update.mjs";
 | 
			
		||||
 | 
			
		||||
const notionSidebar = `.notion-sidebar-container .notion-sidebar > :nth-child(3) > div > :nth-child(2)`;
 | 
			
		||||
 | 
			
		||||
function SidebarButton(
 | 
			
		||||
  { icon, notifications, themeOverridesLoaded, ...props },
 | 
			
		||||
  ...children
 | 
			
		||||
) {
 | 
			
		||||
  const { html } = globalThis.__enhancerApi;
 | 
			
		||||
  return html`<div
 | 
			
		||||
    tabindex="0"
 | 
			
		||||
    role="button"
 | 
			
		||||
    class="notion-enhancer--menu-button
 | 
			
		||||
    flex select-none cursor-pointer rounded-[3px]
 | 
			
		||||
    text-[14px] my-px mx-[4px] py-[2px] px-[10px]
 | 
			
		||||
    transition hover:bg-[color:var(--theme--bg-hover)]"
 | 
			
		||||
    ...${props}
 | 
			
		||||
  >
 | 
			
		||||
    <div class="flex items-center justify-center w-[22px] h-[22px] mr-[8px]">
 | 
			
		||||
      <i class="i-${icon}"></i>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div>${children}</div>
 | 
			
		||||
 | 
			
		||||
    <div class="ml-auto my-auto${notifications > 0 ? "" : " hidden"}">
 | 
			
		||||
      <!-- accents are squashed into one variable for theming:
 | 
			
		||||
      use rgb to match notion if overrides not loaded -->
 | 
			
		||||
      <div
 | 
			
		||||
        class="flex justify-center w-[16px] h-[16px] font-semibold
 | 
			
		||||
        text-([10px] [color:var(--theme--accent-secondary\\_contrast)])
 | 
			
		||||
        bg-[color:var(--theme--accent-secondary)] rounded-[3px] mb-[2px]
 | 
			
		||||
        dark:bg-[color:${themeOverridesLoaded
 | 
			
		||||
          ? "var(--theme--accent-secondary)"
 | 
			
		||||
          : "rgb(180,65,60)"}]"
 | 
			
		||||
      >
 | 
			
		||||
        <span class="ml-[-0.5px]">${notifications}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async (api, db) => {
 | 
			
		||||
  const {
 | 
			
		||||
      html,
 | 
			
		||||
@ -109,24 +147,14 @@ export default async (api, db) => {
 | 
			
		||||
  </div>`;
 | 
			
		||||
  document.body.append($menuModal);
 | 
			
		||||
 | 
			
		||||
  const $menuButton = html`<div
 | 
			
		||||
  const $menuButton = html`<${SidebarButton}
 | 
			
		||||
    onclick=${openMenu}
 | 
			
		||||
    tabindex="0"
 | 
			
		||||
    role="button"
 | 
			
		||||
    class="notion-enhancer--menu-button
 | 
			
		||||
    flex select-none cursor-pointer rounded-[3px]
 | 
			
		||||
    text-[14px] my-px mx-[4px] py-[2px] px-[10px]
 | 
			
		||||
    transition hover:bg-[color:var(--theme--bg-hover)]"
 | 
			
		||||
  >
 | 
			
		||||
    <div class="flex items-center justify-center w-[22px] h-[22px] mr-[8px]">
 | 
			
		||||
      <i
 | 
			
		||||
        class="i-notion-enhancer${menuButtonIconStyle === "Monochrome"
 | 
			
		||||
          ? "?mask"
 | 
			
		||||
          : " text-[16px]"}"
 | 
			
		||||
      ></i>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div>notion-enhancer</div>
 | 
			
		||||
  </div>`;
 | 
			
		||||
    notifications=${(await checkForUpdate()) ? 1 : 0}
 | 
			
		||||
    icon="notion-enhancer${menuButtonIconStyle === "Monochrome"
 | 
			
		||||
      ? "?mask"
 | 
			
		||||
      : " text-[16px]"}"
 | 
			
		||||
    >notion-enhancer
 | 
			
		||||
  <//>`;
 | 
			
		||||
  addMutationListener(notionSidebar, () => {
 | 
			
		||||
    if (document.contains($menuButton)) return;
 | 
			
		||||
    document.querySelector(notionSidebar)?.append($menuButton);
 | 
			
		||||
 | 
			
		||||
@ -4,10 +4,8 @@
 | 
			
		||||
 * (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;
 | 
			
		||||
  const { html, extendProps } = globalThis.__enhancerApi;
 | 
			
		||||
  extendProps(props, {
 | 
			
		||||
    class: `notion-enhancer--menu-button shrink-0
 | 
			
		||||
    flex gap-[8px] items-center px-[12px] rounded-[4px]
 | 
			
		||||
 | 
			
		||||
@ -4,10 +4,10 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { useState, extendProps } from "../state.mjs";
 | 
			
		||||
import { useState } from "../state.mjs";
 | 
			
		||||
 | 
			
		||||
function Checkbox({ _get, _set, ...props }) {
 | 
			
		||||
  const { html } = globalThis.__enhancerApi,
 | 
			
		||||
  const { html, extendProps } = globalThis.__enhancerApi,
 | 
			
		||||
    $input = html`<input
 | 
			
		||||
      type="checkbox"
 | 
			
		||||
      class="hidden checked:sibling:(px-px
 | 
			
		||||
 | 
			
		||||
@ -4,10 +4,8 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { extendProps } from "../state.mjs";
 | 
			
		||||
 | 
			
		||||
function Description(props, ...children) {
 | 
			
		||||
  const { html } = globalThis.__enhancerApi;
 | 
			
		||||
  const { html, extendProps } = globalThis.__enhancerApi;
 | 
			
		||||
  extendProps(props, {
 | 
			
		||||
    class: `notion-enhancer--menu-description typography
 | 
			
		||||
    leading-[16px] text-([12px] [color:var(--theme--fg-secondary)])`,
 | 
			
		||||
 | 
			
		||||
@ -4,10 +4,8 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { extendProps } from "../state.mjs";
 | 
			
		||||
 | 
			
		||||
function Heading(props, ...children) {
 | 
			
		||||
  const { html } = globalThis.__enhancerApi;
 | 
			
		||||
  const { html, extendProps } = globalThis.__enhancerApi;
 | 
			
		||||
  extendProps(props, {
 | 
			
		||||
    class: `notion-enhancer--menu-heading text-[16px]
 | 
			
		||||
    font-semibold mb-[16px] mt-[48px] first:mt-0 pb-[12px]
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { extendProps, useState } from "../state.mjs";
 | 
			
		||||
import { useState } from "../state.mjs";
 | 
			
		||||
 | 
			
		||||
const updateHotkey = (event) => {
 | 
			
		||||
    const keys = [];
 | 
			
		||||
@ -75,7 +75,7 @@ function Input({
 | 
			
		||||
  ...props
 | 
			
		||||
}) {
 | 
			
		||||
  let $filename, $clear;
 | 
			
		||||
  const { html } = globalThis.__enhancerApi;
 | 
			
		||||
  const { html, extendProps } = globalThis.__enhancerApi;
 | 
			
		||||
  Coloris({ format: "rgb" });
 | 
			
		||||
 | 
			
		||||
  type ??= "text";
 | 
			
		||||
 | 
			
		||||
@ -4,15 +4,15 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { setState, useState, extendProps } from "../state.mjs";
 | 
			
		||||
import { setState, useState } from "../state.mjs";
 | 
			
		||||
 | 
			
		||||
function Popup({ trigger, onopen, onclose, onbeforeclose }, ...children) {
 | 
			
		||||
  const { html } = globalThis.__enhancerApi,
 | 
			
		||||
  const { html, extendProps } = 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"
 | 
			
		||||
      flex-(& col) justify-center items-end z-20
 | 
			
		||||
      pointer-events-none font-normal text-left"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="relative right-[100%]">
 | 
			
		||||
        <div
 | 
			
		||||
@ -34,10 +34,10 @@ function Popup({ trigger, onopen, onclose, onbeforeclose }, ...children) {
 | 
			
		||||
    onopen?.();
 | 
			
		||||
  };
 | 
			
		||||
  $popup.hide = () => {
 | 
			
		||||
    onbeforeclose?.();
 | 
			
		||||
    $popup.removeAttribute("open");
 | 
			
		||||
    $popup.style.pointerEvents = "auto";
 | 
			
		||||
    $popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
 | 
			
		||||
    onbeforeclose?.();
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      $popup.style.pointerEvents = "";
 | 
			
		||||
      setState({ popupOpen: false });
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { useState, extendProps } from "../state.mjs";
 | 
			
		||||
import { useState } from "../state.mjs";
 | 
			
		||||
import { Popup } from "./Popup.mjs";
 | 
			
		||||
 | 
			
		||||
function Option({ value, _get, _set }) {
 | 
			
		||||
 | 
			
		||||
@ -4,10 +4,8 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { extendProps } from "../state.mjs";
 | 
			
		||||
 | 
			
		||||
function Tile({ icon, title, tagName, ...props }, ...children) {
 | 
			
		||||
  const { html } = globalThis.__enhancerApi;
 | 
			
		||||
  const { html, extendProps } = globalThis.__enhancerApi;
 | 
			
		||||
  extendProps(props, {
 | 
			
		||||
    class: `flex items-center gap-[12px] px-[16px] py-[12px]
 | 
			
		||||
    bg-[color:var(--theme--bg-secondary)] hover:bg-[color:var(--theme--bg-hover)]
 | 
			
		||||
 | 
			
		||||
@ -4,10 +4,10 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { useState, extendProps } from "../state.mjs";
 | 
			
		||||
import { useState } from "../state.mjs";
 | 
			
		||||
 | 
			
		||||
function Toggle({ _get, _set, ...props }) {
 | 
			
		||||
  const { html } = globalThis.__enhancerApi,
 | 
			
		||||
  const { html, extendProps } = globalThis.__enhancerApi,
 | 
			
		||||
    $input = html`<input
 | 
			
		||||
      type="checkbox"
 | 
			
		||||
      class="hidden checked:sibling:children:(
 | 
			
		||||
 | 
			
		||||
@ -4,10 +4,14 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Popup } from "../components/Popup.mjs";
 | 
			
		||||
import { Button } from "../components/Button.mjs";
 | 
			
		||||
import { Description } from "../components/Description.mjs";
 | 
			
		||||
import { useState } from "../state.mjs";
 | 
			
		||||
 | 
			
		||||
const updateGuide =
 | 
			
		||||
  "https://notion-enhancer.github.io/getting-started/updating/";
 | 
			
		||||
 | 
			
		||||
const rectToStyle = (rect) =>
 | 
			
		||||
  ["width", "height", "top", "bottom", "left", "right"]
 | 
			
		||||
    .filter((prop) => rect[prop])
 | 
			
		||||
@ -63,9 +67,50 @@ function Circle(rect) {
 | 
			
		||||
  ></div>`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Banner() {
 | 
			
		||||
function Banner({ updateAvailable, isDevelopmentBuild }) {
 | 
			
		||||
  const { html, version, initDatabase } = globalThis.__enhancerApi,
 | 
			
		||||
    $welcome = html`<div
 | 
			
		||||
    $version = html`<button
 | 
			
		||||
      class="text-[12px] py-[2px] px-[6px] mt-[2px]
 | 
			
		||||
      font-medium leading-tight tracking-wide rounded-[3px]
 | 
			
		||||
      relative bg-purple-500 from-white/[0.18] to-white/[0.16]
 | 
			
		||||
      bg-[linear-gradient(225deg,var(--tw-gradient-stops))]"
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        class="notion-enhancer--menu-update-indicator
 | 
			
		||||
        absolute h-[12px] w-[12px] right-[-6px] top-[-6px]
 | 
			
		||||
        ${updateAvailable ? "" : "hidden"}"
 | 
			
		||||
      >
 | 
			
		||||
        <span
 | 
			
		||||
          class="block rounded-full h-full w-full
 | 
			
		||||
          absolute bg-purple-500/75 animate-ping"
 | 
			
		||||
        ></span>
 | 
			
		||||
        <span
 | 
			
		||||
          class="block rounded-full h-full w-full
 | 
			
		||||
          relative bg-purple-500"
 | 
			
		||||
        ></span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <span class="relative">v${version}</span>
 | 
			
		||||
    </button>`,
 | 
			
		||||
    $popup = html`<${Popup} trigger=${$version}>
 | 
			
		||||
      <p
 | 
			
		||||
        class="typography py-[2px] px-[8px] text-[14px]"
 | 
			
		||||
        innerHTML=${updateAvailable
 | 
			
		||||
          ? `<b>v${updateAvailable}</b> is available! <a href="${updateGuide}">Update now.</a>`
 | 
			
		||||
          : isDevelopmentBuild
 | 
			
		||||
          ? "This is a development build of the notion-enhancer. It may be unstable."
 | 
			
		||||
          : "You're up to date!"}
 | 
			
		||||
      />
 | 
			
		||||
    <//>`;
 | 
			
		||||
  $version.append($popup);
 | 
			
		||||
  if (updateAvailable) {
 | 
			
		||||
    useState(["focus", "view"], ([, view = "welcome"]) => {
 | 
			
		||||
      if (view !== "welcome") return;
 | 
			
		||||
      // delayed appearance = movement attracts eye
 | 
			
		||||
      setTimeout(() => $version.lastElementChild.show(), 400);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const $welcome = html`<div
 | 
			
		||||
      class="relative flex overflow-hidden h-[192px] rounded-t-[4px]
 | 
			
		||||
      border-(& purple-400) bg-purple-500 from-white/20 to-transparent
 | 
			
		||||
      text-white bg-[linear-gradient(225deg,var(--tw-gradient-stops))]"
 | 
			
		||||
@ -77,9 +122,8 @@ function Banner() {
 | 
			
		||||
      <${Star} width="36px" height="36px" top="136px" left="190px" />
 | 
			
		||||
      <${Star} width="48px" height="48px" top="32px" left="336px" />
 | 
			
		||||
      <${Star} width="64px" height="64px" top="90px" left="448px" from="lg" />
 | 
			
		||||
 | 
			
		||||
      <h1
 | 
			
		||||
        class="z-10 pl-[32px] md:pl-[48px] lg:pl-[64px]
 | 
			
		||||
        class="z-10 px-[32px] md:px-[48px] lg:px-[64px]
 | 
			
		||||
        font-bold leading-tight tracking-tight my-auto"
 | 
			
		||||
      >
 | 
			
		||||
        <a href="https://notion-enhancer.github.io/">
 | 
			
		||||
@ -87,21 +131,14 @@ function Banner() {
 | 
			
		||||
          <span class="text-[28px]">the notion-enhancer</span>
 | 
			
		||||
        </a>
 | 
			
		||||
      </h1>
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        class="flex flex-col absolute bottom-0 right-0
 | 
			
		||||
        pr-[32px] md:pr-[48px] lg:pr-[64px] pb-[24px]"
 | 
			
		||||
        class="absolute bottom-0 right-0 py-[24px]
 | 
			
		||||
        px-[32px] md:px-[48px] lg:px-[64px]"
 | 
			
		||||
      >
 | 
			
		||||
        <i class="i-notion-enhancer text-[42px] mx-auto mb-[8px]"></i>
 | 
			
		||||
        <a
 | 
			
		||||
          href="https://github.com/notion-enhancer/notion-enhancer/releases/tag/v${version}"
 | 
			
		||||
        >
 | 
			
		||||
          <span
 | 
			
		||||
            class="text-[12px] py-[2px] px-[6px]
 | 
			
		||||
          font-medium leading-tight tracking-wide"
 | 
			
		||||
            >v${version}
 | 
			
		||||
          </span>
 | 
			
		||||
        </a>
 | 
			
		||||
        <div class="relative flex-(& col)">
 | 
			
		||||
          <i class="i-notion-enhancer text-[42px] mx-auto mb-[8px]"></i>
 | 
			
		||||
          ${$version}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>`,
 | 
			
		||||
    $sponsorship = html`<div
 | 
			
		||||
@ -118,16 +155,14 @@ function Banner() {
 | 
			
		||||
          variant="brand"
 | 
			
		||||
          class="grow justify-center"
 | 
			
		||||
          href="https://www.buymeacoffee.com/dragonwocky"
 | 
			
		||||
        >
 | 
			
		||||
          Buy me a coffee
 | 
			
		||||
          >Buy me a coffee
 | 
			
		||||
        <//>
 | 
			
		||||
        <${Button}
 | 
			
		||||
          icon="calendar-heart"
 | 
			
		||||
          variant="brand"
 | 
			
		||||
          class="grow justify-center"
 | 
			
		||||
          href="https://github.com/sponsors/dragonwocky"
 | 
			
		||||
        >
 | 
			
		||||
          Sponsor me
 | 
			
		||||
          >Sponsor me
 | 
			
		||||
        <//>
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- Disclaimer: these perks are only a draft, for anyone reading this.
 | 
			
		||||
@ -140,7 +175,6 @@ function Banner() {
 | 
			
		||||
        the instructions in the <b>#welcome</b> channel.
 | 
			
		||||
      <//>
 | 
			
		||||
    </div>`;
 | 
			
		||||
 | 
			
		||||
  initDatabase()
 | 
			
		||||
    .get("agreedToTerms")
 | 
			
		||||
    .then((agreedToTerms) => {
 | 
			
		||||
 | 
			
		||||
@ -62,7 +62,7 @@ function List({ id, mods, description }) {
 | 
			
		||||
        };
 | 
			
		||||
      return html`<${Mod} ...${{ ...mod, _get, _set }} />`;
 | 
			
		||||
    });
 | 
			
		||||
  return html`<div class="flex flex-col gap-y-[14px]">
 | 
			
		||||
  return html`<div class="flex-(& col) gap-y-[14px]">
 | 
			
		||||
    <${Search} items=${$mods} itemType=${id} />
 | 
			
		||||
    <${Description} innerHTML=${description} />
 | 
			
		||||
    ${$mods}
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ function Mod({
 | 
			
		||||
          class="rounded-[4px] mr-[12px] h-[74px] my-auto"
 | 
			
		||||
        />`
 | 
			
		||||
      : ""}
 | 
			
		||||
    <div class="flex flex-col max-w-[50%]">
 | 
			
		||||
    <div class="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) => {
 | 
			
		||||
@ -45,8 +45,7 @@ function Mod({
 | 
			
		||||
            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}
 | 
			
		||||
            >${tag}
 | 
			
		||||
          </span>`;
 | 
			
		||||
        })}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Heading } from "../components/Heading.mjs";
 | 
			
		||||
import { Description } from "../components/Description.mjs";
 | 
			
		||||
import { Checkbox } from "../components/Checkbox.mjs";
 | 
			
		||||
import { Button } from "../components/Button.mjs";
 | 
			
		||||
@ -68,7 +67,7 @@ function Onboarding() {
 | 
			
		||||
      >Build your own extension.
 | 
			
		||||
    <//>
 | 
			
		||||
    <${Tile}
 | 
			
		||||
      href="https://github.com/notion-enhancer/notion-enhancer/issues/new?template=BUG_REPORT.md"
 | 
			
		||||
      href="https://github.com/notion-enhancer/notion-enhancer/issues"
 | 
			
		||||
      icon="bug"
 | 
			
		||||
      title="Something not working?"
 | 
			
		||||
      >Report a bug.
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@ function Option({ _get, _set, ...opt }) {
 | 
			
		||||
    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%]"}">
 | 
			
		||||
    <div class="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}
 | 
			
		||||
 | 
			
		||||
@ -133,7 +133,7 @@ function Profile({ id }) {
 | 
			
		||||
      <p class="text-[14px] py-[2px] px-[8px]">
 | 
			
		||||
        Are you sure you want to delete the profile ${$confirmName} permanently?
 | 
			
		||||
      </p>
 | 
			
		||||
      <div class="flex flex-col gap-[8px] py-[6px] px-[8px]">
 | 
			
		||||
      <div class="flex-(& col) gap-[8px] py-[6px] px-[8px]">
 | 
			
		||||
        <${Button}
 | 
			
		||||
          tabindex="0"
 | 
			
		||||
          icon="trash"
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { extendProps, setState, useState } from "../state.mjs";
 | 
			
		||||
import { setState, useState } from "../state.mjs";
 | 
			
		||||
import { Description } from "../components/Description.mjs";
 | 
			
		||||
 | 
			
		||||
function SidebarHeading({}, ...children) {
 | 
			
		||||
@ -19,7 +19,7 @@ function SidebarHeading({}, ...children) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SidebarButton({ id, icon, ...props }, ...children) {
 | 
			
		||||
  const { html } = globalThis.__enhancerApi,
 | 
			
		||||
  const { html, extendProps } = globalThis.__enhancerApi,
 | 
			
		||||
    $btn = html`<${props["href"] ? "a" : "button"}
 | 
			
		||||
      class="flex items-center select-none text-[14px]
 | 
			
		||||
      py-[5px] px-[15px] last:mb-[12px] w-full transition
 | 
			
		||||
@ -59,7 +59,7 @@ function Sidebar({ items, categories }) {
 | 
			
		||||
      policy and terms & conditions on the welcome page.
 | 
			
		||||
    </span>`,
 | 
			
		||||
    $sidebar = html`<aside
 | 
			
		||||
      class="notion-enhancer--menu-sidebar flex flex-col row-span-1
 | 
			
		||||
      class="notion-enhancer--menu-sidebar flex-(& col) row-span-1
 | 
			
		||||
      h-full overflow-y-auto bg-[color:var(--theme--bg-secondary)]"
 | 
			
		||||
    >
 | 
			
		||||
      ${items.map((item) => {
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { setState, useState } from "./state.mjs";
 | 
			
		||||
import { checkForUpdate, isDevelopmentBuild } from "../update.mjs";
 | 
			
		||||
import { Sidebar } from "./islands/Sidebar.mjs";
 | 
			
		||||
import { Footer } from "./islands/Footer.mjs";
 | 
			
		||||
import { Banner } from "./islands/Banner.mjs";
 | 
			
		||||
@ -120,12 +121,15 @@ const render = async () => {
 | 
			
		||||
      categories=${categories}
 | 
			
		||||
    />`,
 | 
			
		||||
    $main = html`
 | 
			
		||||
      <main class="flex flex-col overflow-hidden transition-[height]">
 | 
			
		||||
      <main class="flex-(& col) overflow-hidden transition-[height]">
 | 
			
		||||
        <!-- wrappers necessary for transitions and breakpoints -->
 | 
			
		||||
        <div class="grow overflow-auto">
 | 
			
		||||
          <div class="relative h-full w-full">
 | 
			
		||||
            <${View} id="welcome">
 | 
			
		||||
              <${Banner} />
 | 
			
		||||
              <${Banner}
 | 
			
		||||
                updateAvailable=${await checkForUpdate()}
 | 
			
		||||
                isDevelopmentBuild=${await isDevelopmentBuild()}
 | 
			
		||||
              />
 | 
			
		||||
              <${Onboarding} />
 | 
			
		||||
            <//>
 | 
			
		||||
            <${View} id="core">
 | 
			
		||||
@ -152,7 +156,9 @@ const render = async () => {
 | 
			
		||||
  $skeleton.replaceWith($sidebar, $main);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
window.addEventListener("focus", () => setState({ rerender: true }));
 | 
			
		||||
window.addEventListener("focus", () => {
 | 
			
		||||
  setState({ focus: true, rerender: true });
 | 
			
		||||
});
 | 
			
		||||
window.addEventListener("message", (event) => {
 | 
			
		||||
  if (event.data?.namespace !== "notion-enhancer") return;
 | 
			
		||||
  const [hotkey, theme, icon] = useState(["hotkey", "theme", "icon"]);
 | 
			
		||||
 | 
			
		||||
@ -21,21 +21,4 @@ const setState = (state) => {
 | 
			
		||||
    return state;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
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 };
 | 
			
		||||
export { setState, useState };
 | 
			
		||||
 | 
			
		||||
@ -1,448 +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 * as api from '../../api/index.mjs';
 | 
			
		||||
import { notifications, $changelogModal } from './notifications.mjs';
 | 
			
		||||
import { modComponents, options } from './components.mjs';
 | 
			
		||||
import * as router from './router.mjs';
 | 
			
		||||
import './styles.mjs';
 | 
			
		||||
 | 
			
		||||
(async () => {
 | 
			
		||||
  const { env, fs, storage, electron, registry, web, components } = api;
 | 
			
		||||
 | 
			
		||||
  for (const mod of await registry.list((mod) => registry.enabled(mod.id))) {
 | 
			
		||||
    for (let script of mod.js?.menu || []) {
 | 
			
		||||
      script = await import(fs.localPath(`repo/${mod._dir}/${script}`));
 | 
			
		||||
      script.default(api, await registry.db(mod.id));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const errors = await registry.errors();
 | 
			
		||||
  if (errors.length) {
 | 
			
		||||
    console.error('[notion-enhancer] registry errors:');
 | 
			
		||||
    console.table(errors);
 | 
			
		||||
    const $errNotification = await notifications.add({
 | 
			
		||||
      icon: 'alert-circle',
 | 
			
		||||
      message: 'Failed to load mods (check console).',
 | 
			
		||||
      color: 'red',
 | 
			
		||||
    });
 | 
			
		||||
    if (['win32', 'linux', 'darwin'].includes(env.name)) {
 | 
			
		||||
      $errNotification.addEventListener('click', () => electron.browser.openDevTools());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'),
 | 
			
		||||
    profileName = await registry.profileName(),
 | 
			
		||||
    profileDB = await registry.profileDB();
 | 
			
		||||
 | 
			
		||||
  web.addHotkeyListener(await db.get(['hotkey']), env.focusNotion);
 | 
			
		||||
 | 
			
		||||
  globalThis.addEventListener('beforeunload', (_event) => {
 | 
			
		||||
    // trigger input save
 | 
			
		||||
    document.activeElement.blur();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const $main = web.html`<main class="main"></main>`,
 | 
			
		||||
    $sidebar = web.html`<article class="sidebar"></article>`,
 | 
			
		||||
    $options = web.html`<div class="options-container">
 | 
			
		||||
    <p class="options-placeholder">Select a mod to view and configure its options.</p>
 | 
			
		||||
  </div>`,
 | 
			
		||||
    $profile = web.html`<button class="profile-trigger">
 | 
			
		||||
    Profile: ${web.escape(profileName)}
 | 
			
		||||
  </button>`;
 | 
			
		||||
 | 
			
		||||
  // profile
 | 
			
		||||
 | 
			
		||||
  let _$profileConfig;
 | 
			
		||||
  const openProfileMenu = async () => {
 | 
			
		||||
    if (!_$profileConfig) {
 | 
			
		||||
      const profileNames = [
 | 
			
		||||
          ...new Set([
 | 
			
		||||
            ...Object.keys(await storage.get(['profiles'], { default: {} })),
 | 
			
		||||
            profileName,
 | 
			
		||||
          ]),
 | 
			
		||||
        ],
 | 
			
		||||
        $options = profileNames.map(
 | 
			
		||||
          (profile) => web.raw`<option
 | 
			
		||||
          class="select-option"
 | 
			
		||||
          value="${web.escape(profile)}"
 | 
			
		||||
          ${profile === profileName ? 'selected' : ''}
 | 
			
		||||
        >${web.escape(profile)}</option>`
 | 
			
		||||
        ),
 | 
			
		||||
        $select = web.html`<select class="input">
 | 
			
		||||
        <option class="select-option" value="--">-- new --</option>
 | 
			
		||||
        ${$options.join('')}
 | 
			
		||||
      </select>`,
 | 
			
		||||
        $edit = web.html`<input
 | 
			
		||||
        type="text"
 | 
			
		||||
        class="input"
 | 
			
		||||
        value="${web.escape(profileName)}"
 | 
			
		||||
        pattern="/^[A-Za-z0-9_-]+$/"
 | 
			
		||||
      >`,
 | 
			
		||||
        $export = web.html`<button class="profile-export">
 | 
			
		||||
        ${await components.feather('download', { class: 'profile-icon-action' })}
 | 
			
		||||
      </button>`,
 | 
			
		||||
        $import = web.html`<label class="profile-import">
 | 
			
		||||
        <input type="file" class="hidden" accept="application/json">
 | 
			
		||||
        ${await components.feather('upload', { class: 'profile-icon-action' })}
 | 
			
		||||
      </label>`,
 | 
			
		||||
        $save = web.html`<button class="profile-save">
 | 
			
		||||
        ${await components.feather('save', { class: 'profile-icon-text' })} Save
 | 
			
		||||
      </button>`,
 | 
			
		||||
        $delete = web.html`<button class="profile-delete">
 | 
			
		||||
        ${await components.feather('trash-2', { class: 'profile-icon-text' })} Delete
 | 
			
		||||
      </button>`,
 | 
			
		||||
        $error = web.html`<p class="profile-error"></p>`;
 | 
			
		||||
 | 
			
		||||
      $export.addEventListener('click', async (_event) => {
 | 
			
		||||
        const now = new Date(),
 | 
			
		||||
          $a = web.html`<a
 | 
			
		||||
          class="hidden"
 | 
			
		||||
          download="notion-enhancer_${web.escape($select.value)}_${now.getFullYear()}-${
 | 
			
		||||
            now.getMonth() + 1
 | 
			
		||||
          }-${now.getDate()}.json"
 | 
			
		||||
          href="data:text/plain;charset=utf-8,${encodeURIComponent(
 | 
			
		||||
            JSON.stringify(await storage.get(['profiles', $select.value], {}), null, 2)
 | 
			
		||||
          )}"
 | 
			
		||||
        ></a>`;
 | 
			
		||||
        web.render(document.body, $a);
 | 
			
		||||
        $a.click();
 | 
			
		||||
        $a.remove();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      $import.addEventListener('change', (event) => {
 | 
			
		||||
        const file = event.target.files[0],
 | 
			
		||||
          reader = new FileReader();
 | 
			
		||||
        reader.onload = async (progress) => {
 | 
			
		||||
          try {
 | 
			
		||||
            const profileUpload = JSON.parse(progress.currentTarget.result);
 | 
			
		||||
            if (!profileUpload) throw Error;
 | 
			
		||||
            await storage.set(['profiles', $select.value], profileUpload);
 | 
			
		||||
            env.reload();
 | 
			
		||||
          } catch {
 | 
			
		||||
            web.render(web.empty($error), 'Invalid JSON uploaded.');
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
        reader.readAsText(file);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      $select.addEventListener('change', (_event) => {
 | 
			
		||||
        if ($select.value === '--') {
 | 
			
		||||
          $edit.value = '';
 | 
			
		||||
        } else $edit.value = $select.value;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      $save.addEventListener('click', async (_event) => {
 | 
			
		||||
        if (profileNames.includes($edit.value) && $select.value !== $edit.value) {
 | 
			
		||||
          web.render(
 | 
			
		||||
            web.empty($error),
 | 
			
		||||
            `The profile "${web.escape($edit.value)}" already exists.`
 | 
			
		||||
          );
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
        if (!$edit.value || !$edit.value.match(/^[A-Za-z0-9_-]+$/)) {
 | 
			
		||||
          web.render(
 | 
			
		||||
            web.empty($error),
 | 
			
		||||
            'Profile names may not be empty & may only contain letters, numbers, hyphens and underscores.'
 | 
			
		||||
          );
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
        await storage.set(['currentprofile'], $edit.value);
 | 
			
		||||
        if ($select.value === '--') {
 | 
			
		||||
          await storage.set(['profiles', $edit.value], {});
 | 
			
		||||
        } else if ($select.value !== $edit.value) {
 | 
			
		||||
          await storage.set(
 | 
			
		||||
            ['profiles', $edit.value],
 | 
			
		||||
            await storage.get(['profiles', $select.value], {})
 | 
			
		||||
          );
 | 
			
		||||
          await storage.set(['profiles', $select.value], undefined);
 | 
			
		||||
        }
 | 
			
		||||
        env.reload();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      $delete.addEventListener('click', async (_event) => {
 | 
			
		||||
        await storage.set(['profiles', $select.value], undefined);
 | 
			
		||||
        await storage.set(
 | 
			
		||||
          ['currentprofile'],
 | 
			
		||||
          profileNames.find((profile) => profile !== $select.value) || 'default'
 | 
			
		||||
        );
 | 
			
		||||
        env.reload();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      _$profileConfig = web.render(
 | 
			
		||||
        web.html`<div></div>`,
 | 
			
		||||
        web.html`<p class="options-placeholder">
 | 
			
		||||
        Profiles are used to switch entire configurations.<br>
 | 
			
		||||
        Be careful - deleting a profile deletes all configuration
 | 
			
		||||
        related to it.<br>
 | 
			
		||||
      </p>`,
 | 
			
		||||
        web.render(
 | 
			
		||||
          web.html`<label class="input-label"></label>`,
 | 
			
		||||
          $select,
 | 
			
		||||
          web.html`${await components.feather('chevron-down', { class: 'input-icon' })}`
 | 
			
		||||
        ),
 | 
			
		||||
        web.render(
 | 
			
		||||
          web.html`<label class="input-label"></label>`,
 | 
			
		||||
          $edit,
 | 
			
		||||
          web.html`${await components.feather('type', { class: 'input-icon' })}`
 | 
			
		||||
        ),
 | 
			
		||||
        web.render(
 | 
			
		||||
          web.html`<p class="profile-actions"></p>`,
 | 
			
		||||
          $export,
 | 
			
		||||
          $import,
 | 
			
		||||
          $save,
 | 
			
		||||
          $delete
 | 
			
		||||
        ),
 | 
			
		||||
        $error
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    web.render(web.empty($options), _$profileConfig);
 | 
			
		||||
  };
 | 
			
		||||
  $profile.addEventListener('click', () => openSidebarMenu('profile'));
 | 
			
		||||
 | 
			
		||||
  // mods
 | 
			
		||||
 | 
			
		||||
  const $modLists = {},
 | 
			
		||||
    generators = {
 | 
			
		||||
      options: async (mod) => {
 | 
			
		||||
        const $fragment = document.createDocumentFragment();
 | 
			
		||||
        for (const opt of mod.options) {
 | 
			
		||||
          if (!opt.environments.includes(env.name)) continue;
 | 
			
		||||
          web.render($fragment, await options[opt.type](mod, opt));
 | 
			
		||||
        }
 | 
			
		||||
        if (!mod.options.length) {
 | 
			
		||||
          web.render($fragment, web.html`<p class="options-placeholder">No options.</p>`);
 | 
			
		||||
        }
 | 
			
		||||
        return $fragment;
 | 
			
		||||
      },
 | 
			
		||||
      mod: async (mod) => {
 | 
			
		||||
        const $mod = web.html`<div class="mod" data-id="${web.escape(mod.id)}"></div>`,
 | 
			
		||||
          $toggle = modComponents.toggle('', await registry.enabled(mod.id));
 | 
			
		||||
        $toggle.addEventListener('change', async (event) => {
 | 
			
		||||
          if (event.target.checked && mod.tags.includes('theme')) {
 | 
			
		||||
            const mode = mod.tags.includes('light') ? 'light' : 'dark',
 | 
			
		||||
              id = mod.id,
 | 
			
		||||
              mods = await registry.list(
 | 
			
		||||
                async (mod) =>
 | 
			
		||||
                  (await registry.enabled(mod.id)) &&
 | 
			
		||||
                  mod.tags.includes('theme') &&
 | 
			
		||||
                  mod.tags.includes(mode) &&
 | 
			
		||||
                  mod.id !== id
 | 
			
		||||
              );
 | 
			
		||||
            for (const mod of mods) {
 | 
			
		||||
              profileDB.set(['_mods', mod.id], false);
 | 
			
		||||
              document.querySelector(
 | 
			
		||||
                `[data-id="${web.escape(mod.id)}"] .toggle-check`
 | 
			
		||||
              ).checked = false;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          profileDB.set(['_mods', mod.id], event.target.checked);
 | 
			
		||||
          notifications.onChange();
 | 
			
		||||
        });
 | 
			
		||||
        $mod.addEventListener('click', () => openSidebarMenu(mod.id));
 | 
			
		||||
        return web.render(
 | 
			
		||||
          web.html`<article class="mod-container"></article>`,
 | 
			
		||||
          web.render(
 | 
			
		||||
            $mod,
 | 
			
		||||
            mod.preview
 | 
			
		||||
              ? modComponents.preview(
 | 
			
		||||
                  mod.preview.startsWith('http')
 | 
			
		||||
                    ? mod.preview
 | 
			
		||||
                    : fs.localPath(`repo/${mod._dir}/${mod.preview}`)
 | 
			
		||||
                )
 | 
			
		||||
              : '',
 | 
			
		||||
            web.render(
 | 
			
		||||
              web.html`<div class="mod-body"></div>`,
 | 
			
		||||
              web.render(modComponents.title(mod.name), modComponents.version(mod.version)),
 | 
			
		||||
              modComponents.tags(mod.tags),
 | 
			
		||||
              modComponents.description(mod.description),
 | 
			
		||||
              modComponents.authors(mod.authors),
 | 
			
		||||
              mod.environments.includes(env.name) && !registry.core.includes(mod.id)
 | 
			
		||||
                ? $toggle
 | 
			
		||||
                : ''
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
      modList: async (category, message = '') => {
 | 
			
		||||
        if (!$modLists[category]) {
 | 
			
		||||
          const $search = web.html`<input type="search" class="search"
 | 
			
		||||
          placeholder="Search ('/' to focus)">`,
 | 
			
		||||
            $list = web.html`<div class="mods-list"></div>`,
 | 
			
		||||
            mods = await registry.list(
 | 
			
		||||
              (mod) => mod.environments.includes(env.name) && mod.tags.includes(category)
 | 
			
		||||
            );
 | 
			
		||||
          web.addHotkeyListener(['/'], () => $search.focus());
 | 
			
		||||
          $search.addEventListener('input', (_event) => {
 | 
			
		||||
            const query = $search.value.toLowerCase();
 | 
			
		||||
            for (const $mod of $list.children) {
 | 
			
		||||
              const matches = !query || $mod.innerText.toLowerCase().includes(query);
 | 
			
		||||
              $mod.classList[matches ? 'remove' : 'add']('hidden');
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          for (const mod of mods) {
 | 
			
		||||
            mod.tags = mod.tags.filter((tag) => tag !== category);
 | 
			
		||||
            web.render($list, await generators.mod(mod));
 | 
			
		||||
            mod.tags.unshift(category);
 | 
			
		||||
          }
 | 
			
		||||
          $modLists[category] = web.render(
 | 
			
		||||
            web.html`<div></div>`,
 | 
			
		||||
            web.render(
 | 
			
		||||
              web.html`<label class="search-container"></label>`,
 | 
			
		||||
              $search,
 | 
			
		||||
              web.html`${await components.feather('search', { class: 'input-icon' })}`
 | 
			
		||||
            ),
 | 
			
		||||
            message ? web.render(web.html`<p class="main-message"></p>`, message) : '',
 | 
			
		||||
            $list
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        return $modLists[category];
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  async function openModMenu(id) {
 | 
			
		||||
    let $mod;
 | 
			
		||||
    for (const $list of Object.values($modLists)) {
 | 
			
		||||
      $mod = $list.querySelector(`[data-id="${web.escape(id)}"]`);
 | 
			
		||||
      if ($mod) break;
 | 
			
		||||
    }
 | 
			
		||||
    const mod = await registry.get(id);
 | 
			
		||||
    if (!$mod || !mod || $mod.className === 'mod-selected') return;
 | 
			
		||||
 | 
			
		||||
    $mod.className = 'mod-selected';
 | 
			
		||||
    const fragment = [
 | 
			
		||||
      web.render(modComponents.title(mod.name), modComponents.version(mod.version)),
 | 
			
		||||
      modComponents.tags(mod.tags),
 | 
			
		||||
      await generators.options(mod),
 | 
			
		||||
    ];
 | 
			
		||||
    web.render(web.empty($options), ...fragment);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // views
 | 
			
		||||
 | 
			
		||||
  const $notionNavItem = web.html`<h1 class="nav-notion">
 | 
			
		||||
    ${(await fs.getText('media/colour.svg')).replace(
 | 
			
		||||
      /width="\d+" height="\d+"/,
 | 
			
		||||
      `class="nav-notion-icon"`
 | 
			
		||||
    )}
 | 
			
		||||
    <span>notion-enhancer</span>
 | 
			
		||||
  </h1>`;
 | 
			
		||||
  $notionNavItem.addEventListener('click', env.focusNotion);
 | 
			
		||||
 | 
			
		||||
  const $coreNavItem = web.html`<a href="?view=core" class="nav-item">core</a>`,
 | 
			
		||||
    $extensionsNavItem = web.html`<a href="?view=extensions" class="nav-item">extensions</a>`,
 | 
			
		||||
    $themesNavItem = web.html`<a href="?view=themes" class="nav-item">themes</a>`,
 | 
			
		||||
    $integrationsNavItem = web.html`<a href="?view=integrations" class="nav-item">integrations</a>`,
 | 
			
		||||
    $changelogNavItem = web.html`<button class="nav-item nav-changelog">
 | 
			
		||||
    ${await components.feather('clock', { class: 'nav-changelog-icon' })}
 | 
			
		||||
  </button>`;
 | 
			
		||||
  components.addTooltip($changelogNavItem, '**Update changelog & welcome message**');
 | 
			
		||||
  $changelogNavItem.addEventListener('click', () => {
 | 
			
		||||
    $changelogModal.scrollTop = 0;
 | 
			
		||||
    $changelogModal.classList.add('modal-visible');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  web.render(
 | 
			
		||||
    document.body,
 | 
			
		||||
    web.render(
 | 
			
		||||
      web.html`<div class="body-container"></div>`,
 | 
			
		||||
      web.render(
 | 
			
		||||
        web.html`<div class="content-container"></div>`,
 | 
			
		||||
        web.render(
 | 
			
		||||
          web.html`<nav class="nav"></nav>`,
 | 
			
		||||
          $notionNavItem,
 | 
			
		||||
          $coreNavItem,
 | 
			
		||||
          $extensionsNavItem,
 | 
			
		||||
          $themesNavItem,
 | 
			
		||||
          $integrationsNavItem,
 | 
			
		||||
          web.html`<a href="https://notion-enhancer.github.io" target="_blank" class="nav-item">docs</a>`,
 | 
			
		||||
          web.html`<a href="https://discord.gg/sFWPXtA" target="_blank" class="nav-item">community</a>`,
 | 
			
		||||
          $changelogNavItem
 | 
			
		||||
        ),
 | 
			
		||||
        $main
 | 
			
		||||
      ),
 | 
			
		||||
      web.render($sidebar, $profile, $options)
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  function selectNavItem($item) {
 | 
			
		||||
    for (const $selected of document.querySelectorAll('.nav-item-selected')) {
 | 
			
		||||
      $selected.className = 'nav-item';
 | 
			
		||||
    }
 | 
			
		||||
    $item.className = 'nav-item-selected';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await generators.modList(
 | 
			
		||||
    'core',
 | 
			
		||||
    `Core mods provide the basics required for
 | 
			
		||||
   all other extensions and themes to work. They
 | 
			
		||||
   can't be disabled, but they can be configured
 | 
			
		||||
   - just click on a mod to access its options.`
 | 
			
		||||
  );
 | 
			
		||||
  router.addView('core', async () => {
 | 
			
		||||
    web.empty($main);
 | 
			
		||||
    selectNavItem($coreNavItem);
 | 
			
		||||
    return web.render($main, await generators.modList('core'));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  await generators.modList(
 | 
			
		||||
    'extension',
 | 
			
		||||
    `Extensions build on the functionality and layout of
 | 
			
		||||
   the Notion client, modifying and interacting with
 | 
			
		||||
   existing interfaces.`
 | 
			
		||||
  );
 | 
			
		||||
  router.addView('extensions', async () => {
 | 
			
		||||
    web.empty($main);
 | 
			
		||||
    selectNavItem($extensionsNavItem);
 | 
			
		||||
    return web.render($main, await generators.modList('extension'));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  await generators.modList(
 | 
			
		||||
    'theme',
 | 
			
		||||
    `Themes change Notion's colour scheme.
 | 
			
		||||
   Dark themes will only work when Notion is in dark mode,
 | 
			
		||||
   and light themes will only work when Notion is in light mode.
 | 
			
		||||
   Only one theme of each mode can be enabled at a time.`
 | 
			
		||||
  );
 | 
			
		||||
  router.addView('themes', async () => {
 | 
			
		||||
    web.empty($main);
 | 
			
		||||
    selectNavItem($themesNavItem);
 | 
			
		||||
    return web.render($main, await generators.modList('theme'));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  await generators.modList(
 | 
			
		||||
    'integration',
 | 
			
		||||
    web.html`<span class="danger">Integrations are extensions that use an unofficial API
 | 
			
		||||
   to access and modify content. They are used just like
 | 
			
		||||
   normal extensions, but may be more dangerous to use.</span>`
 | 
			
		||||
  );
 | 
			
		||||
  router.addView('integrations', async () => {
 | 
			
		||||
    web.empty($main);
 | 
			
		||||
    selectNavItem($integrationsNavItem);
 | 
			
		||||
    return web.render($main, await generators.modList('integration'));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  router.setDefaultView('extensions');
 | 
			
		||||
 | 
			
		||||
  router.addQueryListener('id', openSidebarMenu);
 | 
			
		||||
  function openSidebarMenu(id) {
 | 
			
		||||
    if (!id) return;
 | 
			
		||||
    id = web.escape(id);
 | 
			
		||||
 | 
			
		||||
    const deselectedMods = `.mod-selected:not([data-id="${id}"])`;
 | 
			
		||||
    for (const $list of Object.values($modLists)) {
 | 
			
		||||
      for (const $selected of $list.querySelectorAll(deselectedMods)) {
 | 
			
		||||
        $selected.className = 'mod';
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    router.updateQuery(`?id=${id}`);
 | 
			
		||||
 | 
			
		||||
    if (id === 'profile') {
 | 
			
		||||
      openProfileMenu();
 | 
			
		||||
    } else openModMenu(id);
 | 
			
		||||
  }
 | 
			
		||||
})();
 | 
			
		||||
@ -1,146 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * notion-enhancer: menu
 | 
			
		||||
 * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { env, fs, storage, web, components } from '../../api/index.mjs';
 | 
			
		||||
import { tw } from './styles.mjs';
 | 
			
		||||
 | 
			
		||||
import '../../dep/markdown-it.min.js';
 | 
			
		||||
const md = markdownit({ linkify: true });
 | 
			
		||||
 | 
			
		||||
const notificationsURL = 'https://notion-enhancer.github.io/notifications.json';
 | 
			
		||||
export const notifications = {
 | 
			
		||||
  $container: web.html`<div class="notifications-container"></div>`,
 | 
			
		||||
  async add({ icon, message, id = undefined, color = undefined, link = undefined }) {
 | 
			
		||||
    const $notification = link
 | 
			
		||||
        ? web.html`<a
 | 
			
		||||
          href="${web.escape(link)}"
 | 
			
		||||
          class="${tw`notification-${color || 'default'}`}"
 | 
			
		||||
          role="alert"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
        ></a>`
 | 
			
		||||
        : web.html`<p
 | 
			
		||||
          class="${tw`notification-${color || 'default'}`}"
 | 
			
		||||
          role="alert"
 | 
			
		||||
          tabindex="0"
 | 
			
		||||
        ></p>`,
 | 
			
		||||
      resolve = async () => {
 | 
			
		||||
        if (id !== undefined) {
 | 
			
		||||
          notifications.cache.push(id);
 | 
			
		||||
          await storage.set(['notifications'], notifications.cache);
 | 
			
		||||
        }
 | 
			
		||||
        $notification.remove();
 | 
			
		||||
      };
 | 
			
		||||
    $notification.addEventListener('click', resolve);
 | 
			
		||||
    $notification.addEventListener('keyup', (event) => {
 | 
			
		||||
      if (['Enter', ' '].includes(event.key)) resolve();
 | 
			
		||||
    });
 | 
			
		||||
    web.render(
 | 
			
		||||
      notifications.$container,
 | 
			
		||||
      web.render(
 | 
			
		||||
        $notification,
 | 
			
		||||
        web.html`<span class="notification-text markdown-inline">
 | 
			
		||||
          ${md.renderInline(message)}
 | 
			
		||||
        </span>`,
 | 
			
		||||
        web.html`${await components.feather(icon, { class: 'notification-icon' })}`
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
    return $notification;
 | 
			
		||||
  },
 | 
			
		||||
  _onChange: false,
 | 
			
		||||
  async onChange() {
 | 
			
		||||
    if (this._onChange) return;
 | 
			
		||||
    this._onChange = true;
 | 
			
		||||
    const $notification = await this.add({
 | 
			
		||||
      icon: 'refresh-cw',
 | 
			
		||||
      message: 'Reload to apply changes.',
 | 
			
		||||
    });
 | 
			
		||||
    $notification.addEventListener('click', env.reload);
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
(async () => {
 | 
			
		||||
  notifications.cache = await storage.get(['notifications'], []);
 | 
			
		||||
  notifications.provider = await fs.getJSON(notificationsURL);
 | 
			
		||||
 | 
			
		||||
  web.render(document.body, notifications.$container);
 | 
			
		||||
  for (const notification of notifications.provider) {
 | 
			
		||||
    const cached = notifications.cache.includes(notification.id),
 | 
			
		||||
      versionMatches = notification.version === env.version,
 | 
			
		||||
      envMatches = !notification.environments || notification.environments.includes(env.name);
 | 
			
		||||
    if (!cached && versionMatches && envMatches) notifications.add(notification);
 | 
			
		||||
  }
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
export const $changelogModal = web.render(
 | 
			
		||||
  web.html`<div class="modal" role="dialog" aria-modal="true">
 | 
			
		||||
    <div class="modal-overlay" aria-hidden="true"></div>
 | 
			
		||||
  </div>`
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
(async () => {
 | 
			
		||||
  const $changelogModalButton = web.html`<button type="button" class="modal-button">
 | 
			
		||||
    Accept & Continue
 | 
			
		||||
  </button>`;
 | 
			
		||||
  $changelogModalButton.addEventListener('click', async () => {
 | 
			
		||||
    $changelogModal.classList.remove('modal-visible');
 | 
			
		||||
    await storage.set(['last_read_changelog'], env.version);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  web.render(
 | 
			
		||||
    $changelogModal,
 | 
			
		||||
    web.render(
 | 
			
		||||
      web.html`<div class="modal-box"></div>`,
 | 
			
		||||
      web.html`<div class="modal-body">
 | 
			
		||||
        <div class="modal-title">
 | 
			
		||||
          ${(await fs.getText('media/colour.svg')).replace(
 | 
			
		||||
            /width="\d+" height="\d+"/,
 | 
			
		||||
            `class="modal-title-icon"`
 | 
			
		||||
          )}
 | 
			
		||||
          <div>
 | 
			
		||||
            <h1 class="modal-title-heading">
 | 
			
		||||
              notion-enhancer v${env.version}
 | 
			
		||||
            </h1>
 | 
			
		||||
            <p class="modal-title-description">
 | 
			
		||||
              an enhancer/customiser for the all-in-one productivity workspace notion.so
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="modal-content">
 | 
			
		||||
          <p>
 | 
			
		||||
            Welcome to the notion-enhancer! For help getting started, check out the
 | 
			
		||||
            <a href="https://notion-enhancer.github.io/getting-started/basic-usage/" class="link" target="_blank">
 | 
			
		||||
            basic usage</a> guide. If you've upgraded from a previous version of the notion-enhancer, you can see
 | 
			
		||||
            what's new <a href="https://notion-enhancer.github.io/about/changelog/" class="link">here</a>.
 | 
			
		||||
          </p>
 | 
			
		||||
          <p>
 | 
			
		||||
            If you spot a bug or have a new feature idea, have a read through the
 | 
			
		||||
            <a href="https://notion-enhancer.github.io/about/contributing/" class="link">Contributing</a>
 | 
			
		||||
            guide to learn how & where to talk to us about it. For extra support, come join our
 | 
			
		||||
            <a href="https://discord.com/invite/sFWPXtA" class="link" target="_blank">Discord community</a>.
 | 
			
		||||
          </p>
 | 
			
		||||
          <p>
 | 
			
		||||
            Maintaining and updating the notion-enhancer does take a lot of time and work,
 | 
			
		||||
            so if you'd like to support future development
 | 
			
		||||
            <a href="https://github.com/sponsors/dragonwocky" class="important-link" target="_blank">
 | 
			
		||||
            please consider making a donation</a>.
 | 
			
		||||
          </p>
 | 
			
		||||
          <p>
 | 
			
		||||
            By clicking "Accept & Continue" below you agree to the notion-enhancer's
 | 
			
		||||
            <a href="https://notion-enhancer.github.io/about/privacy-policy/" class="link">Privacy Policy</a> and
 | 
			
		||||
            <a href="https://notion-enhancer.github.io/about/terms-and-conditions/" class="link">Terms & Conditions</a>.
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>`,
 | 
			
		||||
      web.render(web.html`<div class="modal-actions"></div>`, $changelogModalButton)
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const lastReadChangelog = await storage.get(['last_read_changelog']);
 | 
			
		||||
  web.render(document.body, $changelogModal);
 | 
			
		||||
  if (lastReadChangelog !== env.version) {
 | 
			
		||||
    $changelogModal.classList.add('modal-visible');
 | 
			
		||||
  }
 | 
			
		||||
})();
 | 
			
		||||
							
								
								
									
										44
									
								
								src/core/update.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/core/update.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
/**
 | 
			
		||||
 * notion-enhancer
 | 
			
		||||
 * (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
 | 
			
		||||
 * (https://notion-enhancer.github.io/) under the MIT license
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
let _release;
 | 
			
		||||
const repo = "notion-enhancer/notion-enhancer",
 | 
			
		||||
  endpoint = `https://api.github.com/repos/${repo}/releases/latest`,
 | 
			
		||||
  getRelease = async () => {
 | 
			
		||||
    const { readJson } = globalThis.__enhancerApi;
 | 
			
		||||
    _release ??= (await readJson(endpoint))?.tag_name.replace(/^v/, "");
 | 
			
		||||
    return _release;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
const parseVersion = (semver) => {
 | 
			
		||||
    while (semver.split("-")[0].split(".").length < 3) semver = `0.${semver}`;
 | 
			
		||||
    let [major, minor, patch, build] = semver.split("."),
 | 
			
		||||
      prerelease = patch.split("-")[1]?.split(".")[0];
 | 
			
		||||
    patch = patch.split("-")[0];
 | 
			
		||||
    return [major, minor, patch, prerelease, build]
 | 
			
		||||
      .map((v) => v ?? "")
 | 
			
		||||
      .map((v) => (/^\d+$/.test(v) ? parseInt(v) : v));
 | 
			
		||||
  },
 | 
			
		||||
  greaterThan = (a, b) => {
 | 
			
		||||
    // is a greater than b
 | 
			
		||||
    a = parseVersion(a);
 | 
			
		||||
    b = parseVersion(b);
 | 
			
		||||
    for (let i = 0; i < a.length; i++) {
 | 
			
		||||
      if (a[i] > b[i]) return true;
 | 
			
		||||
      else if (a[i] < b[i]) return false;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
const checkForUpdate = async () => {
 | 
			
		||||
    const { version } = globalThis.__enhancerApi;
 | 
			
		||||
    return greaterThan(await getRelease(), version) ? _release : false;
 | 
			
		||||
  },
 | 
			
		||||
  isDevelopmentBuild = async () => {
 | 
			
		||||
    const { version } = globalThis.__enhancerApi;
 | 
			
		||||
    return !(await checkForUpdate()) && version !== _release;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
export { checkForUpdate, isDevelopmentBuild };
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user