save options in menu

This commit is contained in:
dragonwocky 2021-04-25 23:29:01 +10:00
parent 8d0ce2e777
commit 15b34ef638
6 changed files with 168 additions and 142 deletions
extension
helpers.jslauncher.js
repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e

View File

@ -6,33 +6,56 @@
'use strict';
export const ERROR = Symbol();
export const ERROR = Symbol(),
env = {},
storage = {},
fs = {},
web = {},
fmt = {},
regexers = {},
registry = {};
export const env = {};
env.name = 'extension';
env.version = chrome.runtime.getManifest().version;
env.openEnhancerMenu = () => chrome.runtime.sendMessage({ action: 'openEnhancerMenu' });
env.focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' });
/** - */
export const storage = {};
storage.set = (id, key, value) =>
new Promise((res, rej) => chrome.storage.sync.set({ [`[${id}]${key}`]: value }, res));
storage.get = (id, key, fallback = undefined) =>
storage.get = (namespace, key = undefined, fallback = undefined) =>
new Promise((res, rej) =>
chrome.storage.sync.get([`[${id}]${key}`], (values) =>
res(values[`[${id}]${key}`] ?? fallback)
)
chrome.storage.sync.get([namespace], async (values) => {
values =
values[namespace] && Object.getOwnPropertyNames(values[namespace]).length
? values[namespace]
: await registry.defaults(namespace);
res((key ? values[key] : values) ?? fallback);
})
);
storage.set = (namespace, key, value) =>
new Promise(async (res, rej) => {
const values = await storage.get(namespace, undefined, {});
chrome.storage.sync.set({ [namespace]: { ...values, [key]: value } }, res);
});
storage.reset = (namespace) =>
new Promise((res, rej) => chrome.storage.sync.set({ [namespace]: undefined }, res));
/** - */
fs.getJSON = (path) =>
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
res.json()
);
fs.getText = (path) =>
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
res.text()
);
fs.isFile = async (path) => {
try {
await fetch(chrome.runtime.getURL(path));
return true;
} catch {
return false;
}
};
export const web = {};
web.whenReady = (selectors = [], callback = () => {}) => {
web.whenReady = (selectors = []) => {
return new Promise((res, rej) => {
function onLoad() {
let isReadyInt;
@ -40,7 +63,6 @@ web.whenReady = (selectors = [], callback = () => {}) => {
function isReadyTest() {
if (selectors.every((selector) => document.querySelector(selector))) {
clearInterval(isReadyInt);
callback();
res(true);
}
}
@ -53,11 +75,12 @@ web.whenReady = (selectors = [], callback = () => {}) => {
} else onLoad();
});
};
/**
* @param {string} html
* @returns HTMLElement
*/
web.loadStyleset = (path) => {
document.head.appendChild(
web.createElement(`<link rel="stylesheet" href="${chrome.runtime.getURL(path)}">`)
);
return true;
};
web.createElement = (html) => {
const template = document.createElement('template');
template.innerHTML = html.includes('<pre')
@ -70,25 +93,14 @@ web.createElement = (html) => {
};
web.escapeHtml = (str) =>
str
.replace(/&/g, '&amp;') // (?![^\s]+;)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/'/g, '&#39;')
.replace(/"/g, '&quot;');
// why a tagged template? because it syntax highlights
// https://marketplace.visualstudio.com/items?itemName=bierner.lit-html
web.html = (html, ...templates) => html.map((str) => str + (templates.shift() || '')).join('');
/**
* @param {string} sheet
*/
web.loadStyleset = (sheet) => {
document.head.appendChild(
web.createElement(`<link rel="stylesheet" href="${chrome.runtime.getURL(sheet)}">`)
);
return true;
};
web.html = (html, ...templates) => html.map((str) => str + (templates.shift() ?? '')).join('');
/**
* @param {array} keys
@ -119,10 +131,6 @@ web.hotkeyListener = (keys, callback) => {
web._hotkeys.push({ keys, callback });
};
/** - */
export const fmt = {};
import './dep/prism.js';
fmt.Prism = Prism;
fmt.Prism.manual = true;
@ -188,69 +196,36 @@ fmt.slugger = (heading, slugs = new Set()) => {
return slug;
};
/** - */
export const fs = {};
/**
* @param {string} path
*/
fs.getJSON = (path) =>
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
res.json()
);
fs.getText = (path) =>
fetch(path.startsWith('https://') ? path : chrome.runtime.getURL(path)).then((res) =>
res.text()
);
fs.isFile = async (path) => {
try {
await fetch(chrome.runtime.getURL(path));
return true;
} catch {
return false;
}
regexers.uuid = (str) => {
const match = str.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
if (match && match.length) return true;
error(`invalid uuid ${str}`);
return false;
};
/** - */
export const regexers = {
uuid(str) {
const match = str.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
if (match && match.length) return true;
error(`invalid uuid ${str}`);
return false;
},
semver(str) {
const match = str.match(
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/i
);
if (match && match.length) return true;
error(`invalid semver ${str}`);
return false;
},
email(str) {
const match = str.match(
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i
);
if (match && match.length) return true;
error(`invalid email ${str}`);
return false;
},
url(str) {
const match = str.match(
/^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i
);
if (match && match.length) return true;
error(`invalid url ${str}`);
return false;
},
regexers.semver = (str) => {
const match = str.match(
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/i
);
if (match && match.length) return true;
error(`invalid semver ${str}`);
return false;
};
regexers.email = (str) => {
const match = str.match(
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i
);
if (match && match.length) return true;
error(`invalid email ${str}`);
return false;
};
regexers.url = (str) => {
const match = str.match(
/^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/i
);
if (match && match.length) return true;
error(`invalid url ${str}`);
return false;
};
/** - */
export const registry = {};
registry.validate = async (mod, err, check) => {
let conditions = [
@ -444,8 +419,33 @@ registry.validate = async (mod, err, check) => {
} while (conditions.some((condition) => Array.isArray(condition)));
return conditions;
};
registry.defaults = async (id) => {
const mod = (await registry.get()).find((mod) => mod.id === id);
if (!mod || !mod.options) return {};
const defaults = {};
for (const opt of mod.options) {
switch (opt.type) {
case 'toggle':
defaults[opt.key] = opt.value;
break;
case 'select':
defaults[opt.key] = opt.values[0];
break;
case 'text':
defaults[opt.key] = opt.value;
break;
case 'number':
defaults[opt.key] = opt.value;
break;
case 'file':
defaults[opt.key] = undefined;
break;
}
}
return defaults;
};
registry.get = async (callback = () => {}) => {
registry.get = async () => {
registry._list = [];
if (!registry._errors) registry._errors = [];
for (const dir of await fs.getJSON('repo/registry.json')) {
@ -466,12 +466,9 @@ registry.get = async (callback = () => {}) => {
err('invalid mod.json');
}
}
callback(registry._list);
return registry._list;
};
registry.errors = async (callback = () => {}) => {
registry.errors = async () => {
if (!registry._errors) await registry.get();
callback(registry._errors);
return registry._errors;
};

View File

@ -7,7 +7,7 @@
'use strict';
import(chrome.runtime.getURL('helpers.js')).then(({ web, registry }) => {
web.whenReady([], async () => {
web.whenReady().then(async () => {
for (let mod of await registry.get()) {
for (let sheet of mod.css?.client || []) {
web.loadStyleset(`repo/${mod._dir}/${sheet}`);

View File

@ -51,8 +51,6 @@
display: flex;
}
.enhancer--notifications > div > :last-child > div {
margin-left: auto;
margin-bottom: 2px;
display: inline-flex;
align-items: center;
justify-content: center;

View File

@ -11,7 +11,7 @@ import { env, storage, web, fs } from '../../helpers.js';
const sidebarSelector =
'#notion-app > div > div.notion-cursor-listener > div.notion-sidebar-container > div > div > div > div:nth-child(4)';
web.whenReady([sidebarSelector], async () => {
web.whenReady([sidebarSelector]).then(async () => {
const $enhancerSidebarElement = web.createElement(
web.html`<div class="enhancer--sidebarMenuTrigger" role="button" tabindex="0">
<div>

View File

@ -272,7 +272,7 @@ label > span:not([class]) {
height: 0.8rem;
width: 0.8rem;
left: 0.325rem;
top: 0.225rem;
top: 0.18rem;
position: absolute;
border-radius: 100%;
background: var(--theme--toggle_dot);

View File

@ -87,16 +87,22 @@ components.card = {
},
};
components.options = {
toggle: (id, { key, label, value }) =>
web.createElement(web.html`<label
async toggle(id, { key, label }) {
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="toggle--${web.escapeHtml(`${id}.${key}`)}"
class="library--toggle_label"
>
<input type="checkbox" id="toggle--${web.escapeHtml(`${id}.${key}`)}" />
<input type="checkbox" id="toggle--${web.escapeHtml(`${id}.${key}`)}"
${state ? 'checked' : ''}/>
<p><span>${label}</span><span class="library--toggle"></span></p
></label>`),
select: async (id, { key, label, values }) =>
web.createElement(web.html`<label
></label>`);
opt.addEventListener('change', (event) => storage.set(id, key, event.target.checked));
return opt;
},
async select(id, { key, label, values }) {
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="select--${web.escapeHtml(`${id}.${key}`)}"
class="library--select_label"
>
@ -106,37 +112,47 @@ components.options = {
<select id="select--${web.escapeHtml(`${id}.${key}`)}">
${values.map(
(value) =>
web.html`<option value="${web.escapeHtml(value)}">${web.escapeHtml(
value
)}</option>`
web.html`<option value="${web.escapeHtml(value)}"
${value === state ? 'selected' : ''}>
${web.escapeHtml(value)}</option>`
)}
</select>
</p>
</label>`),
text(id, { key, label, value }) {
const opt = web.createElement(web.html`<label
</label>`);
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
return opt;
},
async text(id, { key, label }) {
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="text--${web.escapeHtml(`${id}.${key}`)}"
class="library--text_label"
>
<p>${label}</p>
<textarea id="text--${web.escapeHtml(`${id}.${key}`)}" rows="1"></textarea>
<textarea id="text--${web.escapeHtml(`${id}.${key}`)}" rows="1">${state}</textarea>
</label>`);
opt.querySelector('textarea').addEventListener('input', (ev) => {
ev.target.style.removeProperty('--txt--scroll-height');
ev.target.style.setProperty('--txt--scroll-height', ev.target.scrollHeight + 'px');
opt.querySelector('textarea').addEventListener('input', (event) => {
event.target.style.removeProperty('--txt--scroll-height');
event.target.style.setProperty('--txt--scroll-height', event.target.scrollHeight + 'px');
});
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
return opt;
},
number: (id, { key, label, value }) =>
web.createElement(web.html`<label
async number(id, { key, label }) {
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="number--${web.escapeHtml(`${id}.${key}`)}"
class="library--number_label"
>
<p>${web.escapeHtml(label)}</p>
<input id="number--${web.escapeHtml(`${id}.${key}`)}" type="number" />
</label>`),
<input id="number--${web.escapeHtml(`${id}.${key}`)}" type="number" value="${state}"/>
</label>`);
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
return opt;
},
async file(id, { key, label, extensions }) {
const opt = web.createElement(web.html`<label
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="file--${web.escapeHtml(`${id}.${key}`)}"
class="library--file_label"
>
@ -145,18 +161,33 @@ components.options = {
id="file--${web.escapeHtml(`${id}.${key}`)}"
${web.escapeHtml(
extensions && extensions.length
? ` accept="${web.escapeHtml(extensions.join(','))}"`
? ` accept=${web.escapeHtml(extensions.join(','))}`
: ''
)}
/>
<p>${web.escapeHtml(label)}</p>
<p class="library--file">
<span><i data-icon="fa/file"></i></span>
<span class="library--file_path">choose file...</span>
<span class="library--file_path">${state || 'choose file...'}</span>
</p>
<p class="library--warning">
warning: browser extensions do not have true filesystem access,
so the content of the file is saved on selection. after editing it,
the file will need to be re-selected.
</p>
</label>`);
opt.querySelector('input').addEventListener('change', (ev) => {
opt.querySelector('.library--file_path').innerText = ev.target.files[0].name;
opt.addEventListener('change', (event) => {
const file = event.target.files[0],
reader = new FileReader();
opt.querySelector('.library--file_path').innerText = file.name;
storage.set(id, key, file.name);
reader.onload = (progress) => {
storage.set(id, `_file.${key}`, progress.currentTarget.result);
};
reader.readAsText(file);
});
opt.addEventListener('click', (event) => {
document.documentElement.scrollTop = 0;
});
return opt;
},
@ -278,7 +309,7 @@ const views = {
}, 50);
document
.querySelectorAll('img')
.forEach((img) => (img.onerror = (ev) => ev.target.remove()));
.forEach((img) => (img.onerror = (event) => event.target.remove()));
document
.querySelectorAll('a[href^="?"]')
.forEach((a) => a.addEventListener('click', this._router));
@ -310,12 +341,12 @@ const views = {
views._router = views._router.bind(views);
views._navigator = views._navigator.bind(views);
views._load();
window.addEventListener('popstate', (ev) => {
if (ev.state) views._load();
window.addEventListener('popstate', (event) => {
if (event.state) views._load();
});
const notifications = {
_generate({ heading, message = '', type = 'information' }, callback = () => {}) {
_generate({ heading, message = '', type = 'information' }, onDismiss = () => {}) {
let svg = '',
className = 'notification';
switch (type) {
@ -347,7 +378,7 @@ const notifications = {
$notif.offsetHeight / parseFloat(getComputedStyle(document.documentElement).fontSize)
}rem`;
setTimeout(() => $notif.remove(), 400);
callback();
onDismiss();
});
setTimeout(() => {
$notif.style.opacity = 1;
@ -355,7 +386,7 @@ const notifications = {
return $notif;
},
async load() {
let notifications = {
const notifications = {
list: await fs.getJSON('https://notion-enhancer.github.io/notifications.json'),
dismissed: await storage.get(_id, 'notifications', []),
};