scope all await statements

This commit is contained in:
dragonwocky 2021-11-07 18:53:18 +11:00
parent 496305597a
commit a72441ee86
4 changed files with 415 additions and 383 deletions

View File

@ -8,7 +8,6 @@
import { fmt, web, registry, components } from '../../api/_.mjs'; import { fmt, web, registry, components } from '../../api/_.mjs';
import { notifications } from './notifications.mjs'; import { notifications } from './notifications.mjs';
const profileDB = await registry.profileDB();
export const modComponents = { export const modComponents = {
preview: (url) => web.html`<img preview: (url) => web.html`<img
@ -56,7 +55,8 @@ export const modComponents = {
export const options = { export const options = {
toggle: async (mod, opt) => { toggle: async (mod, opt) => {
const checked = await profileDB.get([mod.id, opt.key], opt.value), const profileDB = await registry.profileDB(),
checked = await profileDB.get([mod.id, opt.key], opt.value),
$toggle = modComponents.toggle(opt.label, checked), $toggle = modComponents.toggle(opt.label, checked),
$tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`, $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
$label = $toggle.children[0], $label = $toggle.children[0],
@ -71,8 +71,10 @@ export const options = {
}); });
return $toggle; return $toggle;
}, },
select: async (mod, opt) => { select: async (mod, opt) => {
const value = await profileDB.get([mod.id, opt.key], opt.values[0]), const profileDB = await registry.profileDB(),
value = await profileDB.get([mod.id, opt.key], opt.values[0]),
$tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`, $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
$label = web.render( $label = web.render(
web.html`<label class="input-label"></label>`, web.html`<label class="input-label"></label>`,
@ -96,8 +98,10 @@ export const options = {
}); });
return web.render($label, $select, $icon); return web.render($label, $select, $icon);
}, },
text: async (mod, opt) => { text: async (mod, opt) => {
const value = await profileDB.get([mod.id, opt.key], opt.value), const profileDB = await registry.profileDB(),
value = await profileDB.get([mod.id, opt.key], opt.value),
$tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`, $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
$label = web.render( $label = web.render(
web.html`<label class="input-label"></label>`, web.html`<label class="input-label"></label>`,
@ -112,8 +116,10 @@ export const options = {
}); });
return web.render($label, $input, $icon); return web.render($label, $input, $icon);
}, },
number: async (mod, opt) => { number: async (mod, opt) => {
const value = await profileDB.get([mod.id, opt.key], opt.value), const profileDB = await registry.profileDB(),
value = await profileDB.get([mod.id, opt.key], opt.value),
$tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`, $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
$label = web.render( $label = web.render(
web.html`<label class="input-label"></label>`, web.html`<label class="input-label"></label>`,
@ -128,8 +134,10 @@ export const options = {
}); });
return web.render($label, $input, $icon); return web.render($label, $input, $icon);
}, },
color: async (mod, opt) => { color: async (mod, opt) => {
const value = await profileDB.get([mod.id, opt.key], opt.value), const profileDB = await registry.profileDB(),
value = await profileDB.get([mod.id, opt.key], opt.value),
$tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`, $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
$label = web.render( $label = web.render(
web.html`<label class="input-label"></label>`, web.html`<label class="input-label"></label>`,
@ -166,8 +174,10 @@ export const options = {
paint(); paint();
return web.render($label, $input, $icon); return web.render($label, $input, $icon);
}, },
file: async (mod, opt) => { file: async (mod, opt) => {
const { filename } = (await profileDB.get([mod.id, opt.key], {})) || {}, const profileDB = await registry.profileDB(),
{ filename } = (await profileDB.get([mod.id, opt.key], {})) || {},
$tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`, $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
$label = web.render( $label = web.render(
web.html`<label class="input-label"></label>`, web.html`<label class="input-label"></label>`,
@ -204,8 +214,10 @@ export const options = {
$latest $latest
); );
}, },
hotkey: async (mod, opt) => { hotkey: async (mod, opt) => {
const value = await profileDB.get([mod.id, opt.key], opt.value), const profileDB = await registry.profileDB(),
value = await profileDB.get([mod.id, opt.key], opt.value),
$tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`, $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
$label = web.render( $label = web.render(
web.html`<label class="input-label"></label>`, web.html`<label class="input-label"></label>`,

View File

@ -8,184 +8,185 @@
import * as api from '../../api/_.mjs'; import * as api from '../../api/_.mjs';
import { notifications, $changelogModal } from './notifications.mjs'; import { notifications, $changelogModal } from './notifications.mjs';
const { env, fs, storage, registry, web, components } = api;
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}`);
}
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.log('[notion-enhancer] registry errors:');
console.table(errors);
notifications.add({
icon: 'alert-circle',
message: 'Failed to load mods (check console).',
color: 'red',
});
}
import { modComponents, options } from './components.mjs'; import { modComponents, options } from './components.mjs';
import * as router from './router.mjs'; import * as router from './router.mjs';
import './styles.mjs'; import './styles.mjs';
const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'), (async () => {
profileName = await registry.profileName(), const { env, fs, storage, registry, web, components } = api;
profileDB = await registry.profileDB();
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}`);
}
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.log('[notion-enhancer] registry errors:');
console.table(errors);
notifications.add({
icon: 'alert-circle',
message: 'Failed to load mods (check console).',
color: 'red',
});
}
const loadTheme = async () => { const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'),
document.documentElement.className = profileName = await registry.profileName(),
(await storage.get(['theme'], 'light')) === 'dark' ? 'dark' : ''; profileDB = await registry.profileDB();
};
document.addEventListener('visibilitychange', loadTheme);
loadTheme();
window.addEventListener('beforeunload', (event) => { web.addHotkeyListener(await db.get(['hotkey']), env.focusNotion);
// trigger input save
document.activeElement.blur();
});
const $main = web.html`<main class="main"></main>`, const loadTheme = async () => {
$sidebar = web.html`<article class="sidebar"></article>`, document.documentElement.className =
$options = web.html`<div class="options-container"> (await storage.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>`,
$options = web.html`<div class="options-container">
<p class="options-placeholder">Select a mod to view and configure its options.</p> <p class="options-placeholder">Select a mod to view and configure its options.</p>
</div>`, </div>`,
$profile = web.html`<button class="profile-trigger"> $profile = web.html`<button class="profile-trigger">
Profile: ${web.escape(profileName)} Profile: ${web.escape(profileName)}
</button>`; </button>`;
// profile // profile
let _$profileConfig; let _$profileConfig;
const openProfileMenu = async () => { const openProfileMenu = async () => {
if (!_$profileConfig) { if (!_$profileConfig) {
const profileNames = [ const profileNames = [
...new Set([ ...new Set([
...Object.keys(await storage.get(['profiles'], { default: {} })), ...Object.keys(await storage.get(['profiles'], { default: {} })),
profileName, profileName,
]), ]),
], ],
$options = profileNames.map( $options = profileNames.map(
(profile) => web.raw`<option (profile) => web.raw`<option
class="select-option" class="select-option"
value="${web.escape(profile)}" value="${web.escape(profile)}"
${profile === profileName ? 'selected' : ''} ${profile === profileName ? 'selected' : ''}
>${web.escape(profile)}</option>` >${web.escape(profile)}</option>`
), ),
$select = web.html`<select class="input"> $select = web.html`<select class="input">
<option class="select-option" value="--">-- new --</option> <option class="select-option" value="--">-- new --</option>
${$options.join('')} ${$options.join('')}
</select>`, </select>`,
$edit = web.html`<input $edit = web.html`<input
type="text" type="text"
class="input" class="input"
value="${web.escape(profileName)}" value="${web.escape(profileName)}"
pattern="/^[A-Za-z0-9_-]+$/" pattern="/^[A-Za-z0-9_-]+$/"
>`, >`,
$export = web.html`<button class="profile-export"> $export = web.html`<button class="profile-export">
${await components.feather('download', { class: 'profile-icon-action' })} ${await components.feather('download', { class: 'profile-icon-action' })}
</button>`, </button>`,
$import = web.html`<label class="profile-import"> $import = web.html`<label class="profile-import">
<input type="file" class="hidden" accept="application/json"> <input type="file" class="hidden" accept="application/json">
${await components.feather('upload', { class: 'profile-icon-action' })} ${await components.feather('upload', { class: 'profile-icon-action' })}
</label>`, </label>`,
$save = web.html`<button class="profile-save"> $save = web.html`<button class="profile-save">
${await components.feather('save', { class: 'profile-icon-text' })} Save ${await components.feather('save', { class: 'profile-icon-text' })} Save
</button>`, </button>`,
$delete = web.html`<button class="profile-delete"> $delete = web.html`<button class="profile-delete">
${await components.feather('trash-2', { class: 'profile-icon-text' })} Delete ${await components.feather('trash-2', { class: 'profile-icon-text' })} Delete
</button>`, </button>`,
$error = web.html`<p class="profile-error"></p>`; $error = web.html`<p class="profile-error"></p>`;
$export.addEventListener('click', async (event) => { $export.addEventListener('click', async (event) => {
const now = new Date(), const now = new Date(),
$a = web.html`<a $a = web.html`<a
class="hidden" class="hidden"
download="notion-enhancer_${web.escape($select.value)}_${now.getFullYear()}-${ download="notion-enhancer_${web.escape($select.value)}_${now.getFullYear()}-${
now.getMonth() + 1 now.getMonth() + 1
}-${now.getDate()}.json" }-${now.getDate()}.json"
href="data:text/plain;charset=utf-8,${encodeURIComponent( href="data:text/plain;charset=utf-8,${encodeURIComponent(
JSON.stringify(await storage.get(['profiles', $select.value], {}), null, 2) JSON.stringify(await storage.get(['profiles', $select.value], {}), null, 2)
)}" )}"
></a>`; ></a>`;
web.render(document.body, $a); web.render(document.body, $a);
$a.click(); $a.click();
$a.remove(); $a.remove();
}); });
$import.addEventListener('change', (event) => { $import.addEventListener('change', (event) => {
const file = event.target.files[0], const file = event.target.files[0],
reader = new FileReader(); reader = new FileReader();
reader.onload = async (progress) => { reader.onload = async (progress) => {
try { try {
const profileUpload = JSON.parse(progress.currentTarget.result); const profileUpload = JSON.parse(progress.currentTarget.result);
if (!profileUpload) throw Error; if (!profileUpload) throw Error;
await storage.set(['profiles', $select.value], profileUpload); await storage.set(['profiles', $select.value], profileUpload);
env.reload(); env.reload();
} catch { } catch {
web.render(web.empty($error), 'Invalid JSON uploaded.'); 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) {
reader.readAsText(file); 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();
});
$select.addEventListener('change', async (event) => { $delete.addEventListener('click', 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); await storage.set(['profiles', $select.value], undefined);
} await storage.set(
env.reload(); ['currentprofile'],
}); profileNames.find((profile) => profile !== $select.value) || 'default'
);
env.reload();
});
$delete.addEventListener('click', async (event) => { _$profileConfig = web.render(
await storage.set(['profiles', $select.value], undefined); web.html`<div></div>`,
await storage.set( web.html`<p class="options-placeholder">
['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. Profiles are used to switch entire configurations.
Here they can be selected, renamed or deleted. Here they can be selected, renamed or deleted.
Profile names can only contain letters, numbers, Profile names can only contain letters, numbers,
@ -193,261 +194,268 @@ const openProfileMenu = async () => {
Be careful - deleting a profile deletes all configuration Be careful - deleting a profile deletes all configuration
related to it. related to it.
</p>`, </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(
(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`<article class="mod-container"></article>`,
web.render( web.render(
$mod, web.html`<label class="input-label"></label>`,
mod.preview $select,
? modComponents.preview( web.html`${await components.feather('chevron-down', { class: 'input-icon' })}`
mod.preview.startsWith('http') ),
? mod.preview web.render(
: fs.localPath(`repo/${mod._dir}/${mod.preview}`) web.html`<label class="input-label"></label>`,
) $edit,
: '', web.html`${await components.feather('type', { class: 'input-icon' })}`
web.render( ),
web.html`<div class="mod-body"></div>`, web.render(
web.render(modComponents.title(mod.name), modComponents.version(mod.version)), web.html`<p class="profile-actions"></p>`,
modComponents.tags(mod.tags), $export,
modComponents.description(mod.description), $import,
modComponents.authors(mod.authors), $save,
mod.environments.includes(env.name) && !registry.core.includes(mod.id) $delete
? $toggle ),
: '' $error
)
)
); );
}, }
modList: async (category, message = '') => { web.render(web.empty($options), _$profileConfig);
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];
},
}; };
$profile.addEventListener('click', () => openSidebarMenu('profile'));
async function openModMenu(id) { // mods
let $mod;
for (const $list of Object.values($modLists)) { const $modLists = {},
$mod = $list.querySelector(`[data-id="${web.escape(id)}"]`); generators = {
if ($mod) break; 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(
(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`<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);
} }
const mod = await registry.get(id);
if (!$mod || !mod || $mod.className === 'mod-selected') return;
$mod.className = 'mod-selected'; // views
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">
const $notionNavItem = web.html`<h1 class="nav-notion">
${(await fs.getText('media/colour.svg')).replace( ${(await fs.getText('media/colour.svg')).replace(
/width="\d+" height="\d+"/, /width="\d+" height="\d+"/,
`class="nav-notion-icon"` `class="nav-notion-icon"`
)} )}
<span>notion-enhancer</span> <span>notion-enhancer</span>
</h1>`; </h1>`;
$notionNavItem.addEventListener('click', env.focusNotion); $notionNavItem.addEventListener('click', env.focusNotion);
const $coreNavItem = web.html`<a href="?view=core" class="nav-item">core</a>`, 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>`, $extensionsNavItem = web.html`<a href="?view=extensions" class="nav-item">extensions</a>`,
$themesNavItem = web.html`<a href="?view=themes" class="nav-item">themes</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>`, $integrationsNavItem = web.html`<a href="?view=integrations" class="nav-item">integrations</a>`,
$changelogNavItem = web.html`<button class="nav-item nav-changelog"> $changelogNavItem = web.html`<button class="nav-item nav-changelog">
${await components.feather('clock', { class: 'nav-changelog-icon' })} ${await components.feather('clock', { class: 'nav-changelog-icon' })}
</button>`; </button>`;
$changelogNavItem.addEventListener('click', () => { $changelogNavItem.addEventListener('click', () => {
$changelogModal.scrollTop = 0; $changelogModal.scrollTop = 0;
$changelogModal.classList.add('modal-visible'); $changelogModal.classList.add('modal-visible');
}); });
web.render(
document.body,
web.render( web.render(
web.html`<div class="body-container"></div>`, document.body,
web.render( web.render(
web.html`<div class="content-container"></div>`, web.html`<div class="body-container"></div>`,
web.render( web.render(
web.html`<nav class="nav"></nav>`, web.html`<div class="content-container"></div>`,
$notionNavItem, web.render(
$coreNavItem, web.html`<nav class="nav"></nav>`,
$extensionsNavItem, $notionNavItem,
$themesNavItem, $coreNavItem,
$integrationsNavItem, $extensionsNavItem,
web.html`<a href="https://notion-enhancer.github.io" target="_blank" class="nav-item">docs</a>`, $themesNavItem,
web.html`<a href="https://discord.gg/sFWPXtA" target="_blank" class="nav-item">community</a>`, $integrationsNavItem,
$changelogNavItem 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
), ),
$main web.render($sidebar, $profile, $options)
), )
web.render($sidebar, $profile, $options) );
)
);
function selectNavItem($item) { function selectNavItem($item) {
for (const $selected of document.querySelectorAll('.nav-item-selected')) { for (const $selected of document.querySelectorAll('.nav-item-selected')) {
$selected.className = 'nav-item'; $selected.className = 'nav-item';
}
$item.className = 'nav-item-selected';
} }
$item.className = 'nav-item-selected';
}
await generators.modList( await generators.modList(
'core', 'core',
`Core mods provide the basics required for `Core mods provide the basics required for
all other extensions and themes to work. They all other extensions and themes to work. They
can't be disabled, but they can be configured can't be disabled, but they can be configured
- just click on a mod to access its options.` - just click on a mod to access its options.`
); );
router.addView('core', async () => { router.addView('core', async () => {
web.empty($main); web.empty($main);
selectNavItem($coreNavItem); selectNavItem($coreNavItem);
return web.render($main, await generators.modList('core')); return web.render($main, await generators.modList('core'));
}); });
await generators.modList( await generators.modList(
'extension', 'extension',
`Extensions build on the functionality and layout of `Extensions build on the functionality and layout of
the Notion client, modifying and interacting with the Notion client, modifying and interacting with
existing interfaces.` existing interfaces.`
); );
router.addView('extensions', async () => { router.addView('extensions', async () => {
web.empty($main); web.empty($main);
selectNavItem($extensionsNavItem); selectNavItem($extensionsNavItem);
return web.render($main, await generators.modList('extension')); return web.render($main, await generators.modList('extension'));
}); });
await generators.modList( await generators.modList(
'theme', 'theme',
`Themes change Notion's colour scheme. `Themes change Notion's colour scheme.
Dark themes will only work when Notion is in dark mode, Dark themes will only work when Notion is in dark mode,
and light themes will only work when Notion is in light 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.` Only one theme of each mode can be enabled at a time.`
); );
router.addView('themes', async () => { router.addView('themes', async () => {
web.empty($main); web.empty($main);
selectNavItem($themesNavItem); selectNavItem($themesNavItem);
return web.render($main, await generators.modList('theme')); return web.render($main, await generators.modList('theme'));
}); });
await generators.modList( await generators.modList(
'integration', 'integration',
web.html`<span class="danger">Integrations are extensions that use an unofficial API web.html`<span class="danger">Integrations are extensions that use an unofficial API
to access and modify content. They are used just like to access and modify content. They are used just like
normal extensions, but may be more dangerous to use.</span>` normal extensions, but may be more dangerous to use.</span>`
); );
router.addView('integrations', async () => { router.addView('integrations', async () => {
web.empty($main); web.empty($main);
selectNavItem($integrationsNavItem); selectNavItem($integrationsNavItem);
return web.render($main, await generators.modList('integration')); return web.render($main, await generators.modList('integration'));
}); });
router.setDefaultView('extensions'); router.setDefaultView('extensions');
router.addQueryListener('id', openSidebarMenu); router.addQueryListener('id', openSidebarMenu);
async function openSidebarMenu(id) { async function openSidebarMenu(id) {
if (!id) return; if (!id) return;
id = web.escape(id); id = web.escape(id);
const deselectedMods = `.mod-selected:not([data-id="${id}"])`; const deselectedMods = `.mod-selected:not([data-id="${id}"])`;
for (const $list of Object.values($modLists)) { for (const $list of Object.values($modLists)) {
for (const $selected of $list.querySelectorAll(deselectedMods)) { for (const $selected of $list.querySelectorAll(deselectedMods)) {
$selected.className = 'mod'; $selected.className = 'mod';
}
} }
} router.updateQuery(`?id=${id}`);
router.updateQuery(`?id=${id}`);
if (id === 'profile') { if (id === 'profile') {
openProfileMenu(); openProfileMenu();
} else openModMenu(id); } else openModMenu(id);
} }
})();

View File

@ -10,8 +10,6 @@ import { tw } from './styles.mjs';
const notificationsURL = 'https://notion-enhancer.github.io/notifications.json'; const notificationsURL = 'https://notion-enhancer.github.io/notifications.json';
export const notifications = { export const notifications = {
$container: web.html`<div class="notifications-container"></div>`, $container: web.html`<div class="notifications-container"></div>`,
cache: await storage.get(['notifications'], []),
provider: await fs.getJSON(notificationsURL),
async add({ icon, message, id = undefined, color = undefined, link = undefined }) { async add({ icon, message, id = undefined, color = undefined, link = undefined }) {
const $notification = link const $notification = link
? web.html`<a ? web.html`<a
@ -59,25 +57,40 @@ export const notifications = {
$notification.addEventListener('click', env.reload); $notification.addEventListener('click', env.reload);
}, },
}; };
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);
}
const lastReadChangelog = await storage.get(['last_read_changelog']), (async () => {
$changelogModalButton = web.html`<button type="button" class="modal-button"> notifications.cache = await storage.get(['notifications'], []);
Accept & Continue notifications.provider = await fs.getJSON(notificationsURL);
</button>`;
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( export const $changelogModal = web.render(
web.html`<div class="modal" role="dialog" aria-modal="true"> web.html`<div class="modal" role="dialog" aria-modal="true">
<div class="modal-overlay" aria-hidden="true"></div> <div class="modal-overlay" aria-hidden="true"></div>
</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( web.render(
web.html`<div class="modal-box"></div>`, $changelogModal,
web.html`<div class="modal-body"> web.render(
web.html`<div class="modal-box"></div>`,
web.html`<div class="modal-body">
<div class="modal-title"> <div class="modal-title">
${(await fs.getText('media/colour.svg')).replace( ${(await fs.getText('media/colour.svg')).replace(
/width="\d+" height="\d+"/, /width="\d+" height="\d+"/,
@ -149,14 +162,13 @@ export const $changelogModal = web.render(
</div> </div>
</div> </div>
</div>`, </div>`,
web.render(web.html`<div class="modal-actions"></div>`, $changelogModalButton) web.render(web.html`<div class="modal-actions"></div>`, $changelogModalButton)
) )
); );
web.render(document.body, $changelogModal);
if (lastReadChangelog !== env.version) { const lastReadChangelog = await storage.get(['last_read_changelog']);
$changelogModal.classList.add('modal-visible'); web.render(document.body, $changelogModal);
} if (lastReadChangelog !== env.version) {
$changelogModalButton.addEventListener('click', async () => { $changelogModal.classList.add('modal-visible');
$changelogModal.classList.remove('modal-visible'); }
await storage.set(['last_read_changelog'], env.version); })();
});

View File

@ -165,7 +165,7 @@ body,
.notion-body.dark .notion-body.dark
.notion-default-overlay-container .notion-default-overlay-container
[style*='grid-template-columns: [boolean-start] 60px [boolean-end property-start] 120px [property-end opererator-start] 110px [operator-end value-start] auto [value-end menu-start] 32px [menu-end];'], [style*='grid-template-columns: [boolean-start] 60px [boolean-end property-start] 120px [property-end opererator-start] 110px [operator-end value-start] auto [value-end menu-start] 32px [menu-end];'],
.notion-focusable[style*='background: rgb(80, 85, 88);'], .notion-focusable[style*='background: rgb(80, 85, 88);']:not(.notion-help-button):not(.onboarding-checklist-button),
.notion-body:not(.dark) .notion-body:not(.dark)
.notion-default-overlay-container .notion-default-overlay-container
[style*='grid-template-columns: [boolean-start] 60px [boolean-end property-start] 120px [property-end opererator-start] 110px [operator-end value-start] auto [value-end menu-start] 32px [menu-end];'] [style*='grid-template-columns: [boolean-start] 60px [boolean-end property-start] 120px [property-end opererator-start] 110px [operator-end value-start] auto [value-end menu-start] 32px [menu-end];']