/** * 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); window.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', async (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) { web.render(web.empty($error), 'Profile names cannot be empty.'); return false; } if (!$edit.value.match(/^[A-Za-z0-9_-]+$/)) { web.render( web.empty($error), 'Profile names can only contain letters, numbers, dashes 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. Here they can be selected, renamed or deleted. Profile names can only contain letters, numbers, dashes and underscores.
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( (mod) => mod.environments.includes(env.name) && 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); async 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); } })();