From 3cd8ed770327b6e0001b615a0969159faaa6f249 Mon Sep 17 00:00:00 2001 From: dragonwocky Date: Fri, 3 Feb 2023 01:01:11 +1100 Subject: [PATCH] feat(menu): indicate updates with popup & notification pings --- src/api/interface.js | 20 +- src/core/client.mjs | 62 +++- src/core/menu/components/Button.mjs | 4 +- src/core/menu/components/Checkbox.mjs | 4 +- src/core/menu/components/Description.mjs | 4 +- src/core/menu/components/Heading.mjs | 4 +- src/core/menu/components/Input.mjs | 4 +- src/core/menu/components/Popup.mjs | 10 +- src/core/menu/components/Select.mjs | 2 +- src/core/menu/components/Tile.mjs | 4 +- src/core/menu/components/Toggle.mjs | 4 +- src/core/menu/islands/Banner.mjs | 78 ++-- src/core/menu/islands/List.mjs | 2 +- src/core/menu/islands/Mod.mjs | 5 +- src/core/menu/islands/Onboarding.mjs | 3 +- src/core/menu/islands/Options.mjs | 2 +- src/core/menu/islands/Profiles.mjs | 2 +- src/core/menu/islands/Sidebar.mjs | 6 +- src/core/menu/menu.mjs | 12 +- src/core/menu/state.mjs | 19 +- src/core/menuu/menu.mjs | 448 ----------------------- src/core/menuu/notifications.mjs | 146 -------- src/core/update.mjs | 44 +++ 23 files changed, 199 insertions(+), 690 deletions(-) delete mode 100644 src/core/menuu/menu.mjs delete mode 100644 src/core/menuu/notifications.mjs create mode 100644 src/core/update.mjs diff --git a/src/api/interface.js b/src/api/interface.js index 8748e95..b80814a 100644 --- a/src/api/interface.js +++ b/src/api/interface.js @@ -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 }); diff --git a/src/core/client.mjs b/src/core/client.mjs index 024a05b..bd4542e 100644 --- a/src/core/client.mjs +++ b/src/core/client.mjs @@ -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`
+
+ +
+
${children}
+ +
+ +
+ ${notifications} +
+
+
`; +} + export default async (api, db) => { const { html, @@ -109,24 +147,14 @@ export default async (api, db) => { `; document.body.append($menuModal); - const $menuButton = html`
-
- -
-
notion-enhancer
-
`; + 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); diff --git a/src/core/menu/components/Button.mjs b/src/core/menu/components/Button.mjs index c1b1bf5..42ac31d 100644 --- a/src/core/menu/components/Button.mjs +++ b/src/core/menu/components/Button.mjs @@ -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] diff --git a/src/core/menu/components/Checkbox.mjs b/src/core/menu/components/Checkbox.mjs index 3b00b09..8b49dba 100644 --- a/src/core/menu/components/Checkbox.mjs +++ b/src/core/menu/components/Checkbox.mjs @@ -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` { 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"; diff --git a/src/core/menu/components/Popup.mjs b/src/core/menu/components/Popup.mjs index f67a1c2..d6d74ea 100644 --- a/src/core/menu/components/Popup.mjs +++ b/src/core/menu/components/Popup.mjs @@ -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`
{ + onbeforeclose?.(); $popup.removeAttribute("open"); $popup.style.pointerEvents = "auto"; $popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1)); - onbeforeclose?.(); setTimeout(() => { $popup.style.pointerEvents = ""; setState({ popupOpen: false }); diff --git a/src/core/menu/components/Select.mjs b/src/core/menu/components/Select.mjs index d9262f5..3abb573 100644 --- a/src/core/menu/components/Select.mjs +++ b/src/core/menu/components/Select.mjs @@ -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 }) { diff --git a/src/core/menu/components/Tile.mjs b/src/core/menu/components/Tile.mjs index 732a5b7..7f86e24 100644 --- a/src/core/menu/components/Tile.mjs +++ b/src/core/menu/components/Tile.mjs @@ -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)] diff --git a/src/core/menu/components/Toggle.mjs b/src/core/menu/components/Toggle.mjs index edcd387..ccfd12b 100644 --- a/src/core/menu/components/Toggle.mjs +++ b/src/core/menu/components/Toggle.mjs @@ -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` ["width", "height", "top", "bottom", "left", "right"] .filter((prop) => rect[prop]) @@ -63,9 +67,50 @@ function Circle(rect) { >
`; } -function Banner() { +function Banner({ updateAvailable, isDevelopmentBuild }) { const { html, version, initDatabase } = globalThis.__enhancerApi, - $welcome = html`
+
+ + +
+ v${version} + `, + $popup = html`<${Popup} trigger=${$version}> +

v${updateAvailable} is available! Update now.` + : 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`

<${Star} width="48px" height="48px" top="32px" left="336px" /> <${Star} width="64px" height="64px" top="90px" left="448px" from="lg" /> -

@@ -87,21 +131,14 @@ function Banner() { the notion-enhancer

-
- - - v${version} - - +
+ + ${$version} +
`, $sponsorship = html`
- 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
<${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"]); diff --git a/src/core/menu/state.mjs b/src/core/menu/state.mjs index 892bc85..e0798d2 100644 --- a/src/core/menu/state.mjs +++ b/src/core/menu/state.mjs @@ -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 }; diff --git a/src/core/menuu/menu.mjs b/src/core/menuu/menu.mjs deleted file mode 100644 index ab8105b..0000000 --- a/src/core/menuu/menu.mjs +++ /dev/null @@ -1,448 +0,0 @@ -/** - * notion-enhancer: menu - * (c) 2021 dragonwocky (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`
`, - $sidebar = web.html``, - $options = web.html`
-

Select a mod to view and configure its options.

-
`, - $profile = web.html``; - - // 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`` - ), - $select = web.html``, - $edit = web.html``, - $export = web.html``, - $import = web.html``, - $save = web.html``, - $delete = web.html``, - $error = web.html`

`; - - $export.addEventListener('click', async (_event) => { - const now = new Date(), - $a = web.html``; - 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`
`, - web.html`

- Profiles are used to switch entire configurations.
- Be careful - deleting a profile deletes all configuration - related to it.
-

`, - web.render( - web.html``, - $select, - web.html`${await components.feather('chevron-down', { class: 'input-icon' })}` - ), - web.render( - web.html``, - $edit, - web.html`${await components.feather('type', { class: 'input-icon' })}` - ), - web.render( - web.html`

`, - $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`

No options.

`); - } - return $fragment; - }, - mod: async (mod) => { - const $mod = web.html`
`, - $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`
`, - web.render( - $mod, - mod.preview - ? modComponents.preview( - mod.preview.startsWith('http') - ? mod.preview - : fs.localPath(`repo/${mod._dir}/${mod.preview}`) - ) - : '', - web.render( - web.html`
`, - 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``, - $list = web.html`
`, - 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`
`, - web.render( - web.html``, - $search, - web.html`${await components.feather('search', { class: 'input-icon' })}` - ), - message ? web.render(web.html`

`, 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`

- ${(await fs.getText('media/colour.svg')).replace( - /width="\d+" height="\d+"/, - `class="nav-notion-icon"` - )} - notion-enhancer -

`; - $notionNavItem.addEventListener('click', env.focusNotion); - - const $coreNavItem = web.html`core`, - $extensionsNavItem = web.html`extensions`, - $themesNavItem = web.html`themes`, - $integrationsNavItem = web.html`integrations`, - $changelogNavItem = web.html``; - 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`
`, - web.render( - web.html`
`, - web.render( - web.html``, - $notionNavItem, - $coreNavItem, - $extensionsNavItem, - $themesNavItem, - $integrationsNavItem, - web.html`docs`, - web.html`community`, - $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`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.` - ); - 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); - } -})(); diff --git a/src/core/menuu/notifications.mjs b/src/core/menuu/notifications.mjs deleted file mode 100644 index faf884b..0000000 --- a/src/core/menuu/notifications.mjs +++ /dev/null @@ -1,146 +0,0 @@ -/** - * notion-enhancer: menu - * (c) 2021 dragonwocky (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`
`, - async add({ icon, message, id = undefined, color = undefined, link = undefined }) { - const $notification = link - ? web.html`` - : web.html``, - 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` - ${md.renderInline(message)} - `, - 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`` -); - -(async () => { - const $changelogModalButton = web.html``; - $changelogModalButton.addEventListener('click', async () => { - $changelogModal.classList.remove('modal-visible'); - await storage.set(['last_read_changelog'], env.version); - }); - - web.render( - $changelogModal, - web.render( - web.html``, - web.html``, - web.render(web.html``, $changelogModalButton) - ) - ); - - const lastReadChangelog = await storage.get(['last_read_changelog']); - web.render(document.body, $changelogModal); - if (lastReadChangelog !== env.version) { - $changelogModal.classList.add('modal-visible'); - } -})(); diff --git a/src/core/update.mjs b/src/core/update.mjs new file mode 100644 index 0000000..9293207 --- /dev/null +++ b/src/core/update.mjs @@ -0,0 +1,44 @@ +/** + * notion-enhancer + * (c) 2023 dragonwocky (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 };