notion-enhancer/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/menu.mjs

302 lines
10 KiB
JavaScript

/*
* notion-enhancer core: menu
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
'use strict';
import { env, fs, storage, registry, web } from '../../api/_.mjs';
const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e');
import './styles.mjs';
import { notifications } from './notifications.mjs';
import { components, options } from './components.mjs';
web.addHotkeyListener(await db.get(['hotkey']), env.focusNotion);
for (const mod of await registry.list((mod) => registry.enabled(mod.id))) {
for (const sheet of mod.css?.menu || []) {
web.loadStylesheet(`repo/${mod._dir}/${sheet}`);
}
}
const loadTheme = async () => {
document.documentElement.className =
(await db.get(['theme'], 'light')) === 'dark' ? 'dark' : '';
};
document.addEventListener('visibilitychange', loadTheme);
loadTheme();
window.addEventListener('beforeunload', (event) => {
// trigger input save
document.activeElement.blur();
});
const $main = web.html`<main class="main"></main>`,
$sidebar = web.html`<article class="sidebar"></article>`,
$profile = web.html`<button class="profile-button">
Profile: ${web.escape(registry.profileName)}
</button>`,
$options = web.html`<div class="options-container">
<p class="options-placeholder">Select a mod to view and configure its options.</p>
</div>`;
let _$profileConfig;
$profile.addEventListener('click', async (event) => {
for (const $selected of document.querySelectorAll('.mod-selected')) {
$selected.className = 'mod';
}
if (!_$profileConfig) {
const profileNames = [
...new Set([
...Object.keys(await storage.get(['profiles'], { default: {} })),
registry.profileName,
]),
],
$options = profileNames.map(
(profile) => web.raw`<option
class="select-option"
value="${web.escape(profile)}"
${profile === registry.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(registry.profileName)}"
pattern="/^[A-Za-z0-9_-]+$/"
>`,
$save = web.html`<button class="profile-save">
${web.icon('save', { class: 'button-icon' })} Save
</button>`,
$delete = web.html`<button class="profile-delete">
${web.icon('trash-2', { class: 'button-icon' })} Delete
</button>`,
$error = web.html`<p class="profile-error"></p>`;
$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.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);
}
location.reload();
});
$delete.addEventListener('click', async (event) => {
await storage.set(['profiles', $select.value], undefined);
await storage.set(
['currentprofile'],
profileNames.find((profile) => profile !== $select.value) || 'default'
);
location.reload();
});
_$profileConfig = web.render(
web.html`<div></div>`,
web.html`<p class="options-placeholder">
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. <br>
Be careful - deleting a profile deletes all configuration
related to it.
</p>`,
web.render(
web.html`<label class="input-label"></label>`,
$select,
web.html`${web.icon('chevron-down', { class: 'input-icon' })}`
),
web.render(
web.html`<label class="input-label"></label>`,
$edit,
web.html`${web.icon('type', { class: 'input-icon' })}`
),
web.render(web.html`<p></p>`, $save, $delete),
$error
);
}
web.render(web.empty($options), _$profileConfig);
});
const _$modListCache = {},
generators = {
options: async (mod) => {
const $fragment = document.createDocumentFragment();
for (const opt of mod.options) {
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"></div>`,
$toggle = components.toggle('', await registry.enabled(mod.id));
$toggle.addEventListener('change', (event) => {
registry.profileDB.set(['_mods', mod.id], event.target.checked);
notifications.onChange();
});
$mod.addEventListener('click', async (event) => {
if ($mod.className === 'mod-selected') return;
for (const $selected of document.querySelectorAll('.mod-selected')) {
$selected.className = 'mod';
}
$mod.className = 'mod-selected';
const fragment = [
web.render(components.title(mod.name), components.version(mod.version)),
components.tags(mod.tags),
await generators.options(mod),
];
web.render(web.empty($options), ...fragment);
});
return web.render(
web.html`<article class="mod-container"></article>`,
web.render(
$mod,
mod.preview
? components.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(components.title(mod.name), components.version(mod.version)),
components.tags(mod.tags),
components.description(mod.description),
components.authors(mod.authors),
mod.environments.includes(env.name) && !registry.core.includes(mod.id)
? $toggle
: ''
)
)
);
},
modList: async (category) => {
if (!_$modListCache[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);
}
_$modListCache[category] = web.render(
web.html`<div></div>`,
web.render(
web.html`<label class="search-container"></label>`,
$search,
web.html`${web.icon('search', { class: 'input-icon' })}`
),
$list
);
}
return _$modListCache[category];
},
};
const $notionNavItem = web.html`<h1 class="nav-notion">
${(await fs.getText('icon/colour.svg')).replace(
/width="\d+" height="\d+"/,
`class="nav-notion-icon"`
)}
<a href="https://notion-enhancer.github.io/" target="_blank">notion-enhancer</a>
</h1>`;
$notionNavItem.children[0].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>`,
$communityNavItem = web.html`<a href="https://discord.gg/sFWPXtA" class="nav-item">community</a>`;
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,
$communityNavItem
),
$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';
}
import * as router from './router.mjs';
router.addView('core', async () => {
web.empty($main);
selectNavItem($coreNavItem);
return web.render($main, await generators.modList('core'));
});
router.addView('extensions', async () => {
web.empty($main);
selectNavItem($extensionsNavItem);
return web.render($main, await generators.modList('extension'));
});
router.addView('themes', async () => {
web.empty($main);
selectNavItem($themesNavItem);
return web.render($main, await generators.modList('theme'));
});
router.loadView('extensions', $main);