7007 lines
174 KiB
JavaScript
7007 lines
174 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
/* malevic@0.19.1 - Jan 8, 2022 */
|
|
function m$1(tagOrComponent, props, ...children) {
|
|
props = props || {};
|
|
if (typeof tagOrComponent === "string") {
|
|
const tag = tagOrComponent;
|
|
return { type: tag, props, children };
|
|
}
|
|
if (typeof tagOrComponent === "function") {
|
|
const component = tagOrComponent;
|
|
return { type: component, props, children };
|
|
}
|
|
throw new Error("Unsupported spec type");
|
|
}
|
|
|
|
/* malevic@0.19.1 - Jan 8, 2022 */
|
|
function createPluginsStore() {
|
|
const plugins = [];
|
|
return {
|
|
add(plugin) {
|
|
plugins.push(plugin);
|
|
return this;
|
|
},
|
|
apply(props) {
|
|
let result;
|
|
let plugin;
|
|
const usedPlugins = new Set();
|
|
for (let i = plugins.length - 1; i >= 0; i--) {
|
|
plugin = plugins[i];
|
|
if (usedPlugins.has(plugin)) {
|
|
continue;
|
|
}
|
|
result = plugin(props);
|
|
if (result != null) {
|
|
return result;
|
|
}
|
|
usedPlugins.add(plugin);
|
|
}
|
|
return null;
|
|
},
|
|
delete(plugin) {
|
|
for (let i = plugins.length - 1; i >= 0; i--) {
|
|
if (plugins[i] === plugin) {
|
|
plugins.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
empty() {
|
|
return plugins.length === 0;
|
|
},
|
|
};
|
|
}
|
|
function iterateComponentPlugins(type, pairs, iterator) {
|
|
pairs
|
|
.filter(([key]) => type[key])
|
|
.forEach(([key, plugins]) => {
|
|
return type[key].forEach((plugin) => iterator(plugins, plugin));
|
|
});
|
|
}
|
|
function addComponentPlugins(type, pairs) {
|
|
iterateComponentPlugins(type, pairs, (plugins, plugin) =>
|
|
plugins.add(plugin)
|
|
);
|
|
}
|
|
function deleteComponentPlugins(type, pairs) {
|
|
iterateComponentPlugins(type, pairs, (plugins, plugin) =>
|
|
plugins.delete(plugin)
|
|
);
|
|
}
|
|
function createPluginsAPI(key) {
|
|
const api = {
|
|
add(type, plugin) {
|
|
if (!type[key]) {
|
|
type[key] = [];
|
|
}
|
|
type[key].push(plugin);
|
|
return api;
|
|
},
|
|
};
|
|
return api;
|
|
}
|
|
|
|
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
const PLUGINS_CREATE_ELEMENT = Symbol();
|
|
const pluginsCreateElement = createPluginsStore();
|
|
function createElement(spec, parent) {
|
|
const result = pluginsCreateElement.apply({ spec, parent });
|
|
if (result) {
|
|
return result;
|
|
}
|
|
const tag = spec.type;
|
|
if (tag === "svg") {
|
|
return document.createElementNS(SVG_NS, "svg");
|
|
}
|
|
const namespace = parent.namespaceURI;
|
|
if (namespace === XHTML_NS || namespace == null) {
|
|
return document.createElement(tag);
|
|
}
|
|
return document.createElementNS(namespace, tag);
|
|
}
|
|
|
|
function classes$1(...args) {
|
|
const classes = [];
|
|
args
|
|
.filter((c) => Boolean(c))
|
|
.forEach((c) => {
|
|
if (typeof c === "string") {
|
|
classes.push(c);
|
|
} else if (typeof c === "object") {
|
|
classes.push(...Object.keys(c).filter((key) => Boolean(c[key])));
|
|
}
|
|
});
|
|
return classes.join(" ");
|
|
}
|
|
function setInlineCSSPropertyValue(element, prop, $value) {
|
|
if ($value != null && $value !== "") {
|
|
let value = String($value);
|
|
let important = "";
|
|
if (value.endsWith("!important")) {
|
|
value = value.substring(0, value.length - 10);
|
|
important = "important";
|
|
}
|
|
element.style.setProperty(prop, value, important);
|
|
} else {
|
|
element.style.removeProperty(prop);
|
|
}
|
|
}
|
|
|
|
function isObject(value) {
|
|
return value != null && typeof value === "object";
|
|
}
|
|
|
|
const eventListeners = new WeakMap();
|
|
function addEventListener$1(element, event, listener) {
|
|
let listeners;
|
|
if (eventListeners.has(element)) {
|
|
listeners = eventListeners.get(element);
|
|
} else {
|
|
listeners = new Map();
|
|
eventListeners.set(element, listeners);
|
|
}
|
|
if (listeners.get(event) !== listener) {
|
|
if (listeners.has(event)) {
|
|
element.removeEventListener(event, listeners.get(event));
|
|
}
|
|
element.addEventListener(event, listener);
|
|
listeners.set(event, listener);
|
|
}
|
|
}
|
|
function removeEventListener(element, event) {
|
|
if (!eventListeners.has(element)) {
|
|
return;
|
|
}
|
|
const listeners = eventListeners.get(element);
|
|
element.removeEventListener(event, listeners.get(event));
|
|
listeners.delete(event);
|
|
}
|
|
|
|
function setClassObject(element, classObj) {
|
|
const cls = Array.isArray(classObj)
|
|
? classes$1(...classObj)
|
|
: classes$1(classObj);
|
|
if (cls) {
|
|
element.setAttribute("class", cls);
|
|
} else {
|
|
element.removeAttribute("class");
|
|
}
|
|
}
|
|
function mergeValues(obj, old) {
|
|
const values = new Map();
|
|
const newProps = new Set(Object.keys(obj));
|
|
const oldProps = Object.keys(old);
|
|
oldProps
|
|
.filter((prop) => !newProps.has(prop))
|
|
.forEach((prop) => values.set(prop, null));
|
|
newProps.forEach((prop) => values.set(prop, obj[prop]));
|
|
return values;
|
|
}
|
|
function setStyleObject(element, styleObj, prev) {
|
|
let prevObj;
|
|
if (isObject(prev)) {
|
|
prevObj = prev;
|
|
} else {
|
|
prevObj = {};
|
|
element.removeAttribute("style");
|
|
}
|
|
const declarations = mergeValues(styleObj, prevObj);
|
|
declarations.forEach(($value, prop) =>
|
|
setInlineCSSPropertyValue(element, prop, $value)
|
|
);
|
|
}
|
|
function setEventListener(element, event, listener) {
|
|
if (typeof listener === "function") {
|
|
addEventListener$1(element, event, listener);
|
|
} else {
|
|
removeEventListener(element, event);
|
|
}
|
|
}
|
|
const specialAttrs = new Set([
|
|
"key",
|
|
"oncreate",
|
|
"onupdate",
|
|
"onrender",
|
|
"onremove",
|
|
]);
|
|
const PLUGINS_SET_ATTRIBUTE = Symbol();
|
|
const pluginsSetAttribute = createPluginsStore();
|
|
function getPropertyValue(obj, prop) {
|
|
return obj && obj.hasOwnProperty(prop) ? obj[prop] : null;
|
|
}
|
|
function syncAttrs(element, attrs, prev) {
|
|
const values = mergeValues(attrs, prev || {});
|
|
values.forEach((value, attr) => {
|
|
if (!pluginsSetAttribute.empty()) {
|
|
const result = pluginsSetAttribute.apply({
|
|
element,
|
|
attr,
|
|
value,
|
|
get prev() {
|
|
return getPropertyValue(prev, attr);
|
|
},
|
|
});
|
|
if (result != null) {
|
|
return;
|
|
}
|
|
}
|
|
if (attr === "class" && isObject(value)) {
|
|
setClassObject(element, value);
|
|
} else if (attr === "style" && isObject(value)) {
|
|
const prevValue = getPropertyValue(prev, attr);
|
|
setStyleObject(element, value, prevValue);
|
|
} else if (attr.startsWith("on")) {
|
|
const event = attr.substring(2);
|
|
setEventListener(element, event, value);
|
|
} else if (specialAttrs.has(attr));
|
|
else if (value == null || value === false) {
|
|
element.removeAttribute(attr);
|
|
} else {
|
|
element.setAttribute(attr, value === true ? "" : String(value));
|
|
}
|
|
});
|
|
}
|
|
|
|
class LinkedList {
|
|
constructor(...items) {
|
|
this.nexts = new WeakMap();
|
|
this.prevs = new WeakMap();
|
|
this.first = null;
|
|
this.last = null;
|
|
items.forEach((item) => this.push(item));
|
|
}
|
|
empty() {
|
|
return this.first == null;
|
|
}
|
|
push(item) {
|
|
if (this.empty()) {
|
|
this.first = item;
|
|
this.last = item;
|
|
} else {
|
|
this.nexts.set(this.last, item);
|
|
this.prevs.set(item, this.last);
|
|
this.last = item;
|
|
}
|
|
}
|
|
insertBefore(newItem, refItem) {
|
|
const prev = this.before(refItem);
|
|
this.prevs.set(newItem, prev);
|
|
this.nexts.set(newItem, refItem);
|
|
this.prevs.set(refItem, newItem);
|
|
prev && this.nexts.set(prev, newItem);
|
|
refItem === this.first && (this.first = newItem);
|
|
}
|
|
delete(item) {
|
|
const prev = this.before(item);
|
|
const next = this.after(item);
|
|
prev && this.nexts.set(prev, next);
|
|
next && this.prevs.set(next, prev);
|
|
item === this.first && (this.first = next);
|
|
item === this.last && (this.last = prev);
|
|
}
|
|
before(item) {
|
|
return this.prevs.get(item) || null;
|
|
}
|
|
after(item) {
|
|
return this.nexts.get(item) || null;
|
|
}
|
|
loop(iterator) {
|
|
if (this.empty()) {
|
|
return;
|
|
}
|
|
let current = this.first;
|
|
do {
|
|
if (iterator(current)) {
|
|
break;
|
|
}
|
|
} while ((current = this.after(current)));
|
|
}
|
|
copy() {
|
|
const list = new LinkedList();
|
|
this.loop((item) => {
|
|
list.push(item);
|
|
return false;
|
|
});
|
|
return list;
|
|
}
|
|
forEach(iterator) {
|
|
this.loop((item) => {
|
|
iterator(item);
|
|
return false;
|
|
});
|
|
}
|
|
find(iterator) {
|
|
let result = null;
|
|
this.loop((item) => {
|
|
if (iterator(item)) {
|
|
result = item;
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
return result;
|
|
}
|
|
map(iterator) {
|
|
const results = [];
|
|
this.loop((item) => {
|
|
results.push(iterator(item));
|
|
return false;
|
|
});
|
|
return results;
|
|
}
|
|
}
|
|
|
|
function matchChildren(vnode, old) {
|
|
const oldChildren = old.children();
|
|
const oldChildrenByKey = new Map();
|
|
const oldChildrenWithoutKey = [];
|
|
oldChildren.forEach((v) => {
|
|
const key = v.key();
|
|
if (key == null) {
|
|
oldChildrenWithoutKey.push(v);
|
|
} else {
|
|
oldChildrenByKey.set(key, v);
|
|
}
|
|
});
|
|
const children = vnode.children();
|
|
const matches = [];
|
|
const unmatched = new Set(oldChildren);
|
|
const keys = new Set();
|
|
children.forEach((v) => {
|
|
let match = null;
|
|
let guess = null;
|
|
const key = v.key();
|
|
if (key != null) {
|
|
if (keys.has(key)) {
|
|
throw new Error("Duplicate key");
|
|
}
|
|
keys.add(key);
|
|
if (oldChildrenByKey.has(key)) {
|
|
guess = oldChildrenByKey.get(key);
|
|
}
|
|
} else if (oldChildrenWithoutKey.length > 0) {
|
|
guess = oldChildrenWithoutKey.shift();
|
|
}
|
|
if (v.matches(guess)) {
|
|
match = guess;
|
|
}
|
|
matches.push([v, match]);
|
|
if (match) {
|
|
unmatched.delete(match);
|
|
}
|
|
});
|
|
return { matches, unmatched };
|
|
}
|
|
|
|
function execute(vnode, old, vdom) {
|
|
const didMatch = vnode && old && vnode.matches(old);
|
|
if (didMatch && vnode.parent() === old.parent()) {
|
|
vdom.replaceVNode(old, vnode);
|
|
} else if (vnode) {
|
|
vdom.addVNode(vnode);
|
|
}
|
|
const context = vdom.getVNodeContext(vnode);
|
|
const oldContext = vdom.getVNodeContext(old);
|
|
if (old && !didMatch) {
|
|
old.detach(oldContext);
|
|
old.children().forEach((v) => execute(null, v, vdom));
|
|
old.detached(oldContext);
|
|
}
|
|
if (vnode && !didMatch) {
|
|
vnode.attach(context);
|
|
vnode.children().forEach((v) => execute(v, null, vdom));
|
|
vnode.attached(context);
|
|
}
|
|
if (didMatch) {
|
|
const result = vnode.update(old, context);
|
|
if (result !== vdom.LEAVE) {
|
|
const { matches, unmatched } = matchChildren(vnode, old);
|
|
unmatched.forEach((v) => execute(null, v, vdom));
|
|
matches.forEach(([v, o]) => execute(v, o, vdom));
|
|
vnode.updated(context);
|
|
}
|
|
}
|
|
}
|
|
|
|
function m(tagOrComponent, props, ...children) {
|
|
props = props || {};
|
|
if (typeof tagOrComponent === "string") {
|
|
const tag = tagOrComponent;
|
|
return { type: tag, props, children };
|
|
}
|
|
if (typeof tagOrComponent === "function") {
|
|
const component = tagOrComponent;
|
|
return { type: component, props, children };
|
|
}
|
|
throw new Error("Unsupported spec type");
|
|
}
|
|
function isSpec(x) {
|
|
return isObject(x) && x.type != null && x.nodeType == null;
|
|
}
|
|
function isNodeSpec(x) {
|
|
return isSpec(x) && typeof x.type === "string";
|
|
}
|
|
function isComponentSpec(x) {
|
|
return isSpec(x) && typeof x.type === "function";
|
|
}
|
|
|
|
class VNodeBase {
|
|
constructor(parent) {
|
|
this.parentVNode = parent;
|
|
}
|
|
key() {
|
|
return null;
|
|
}
|
|
parent(vnode) {
|
|
if (vnode) {
|
|
this.parentVNode = vnode;
|
|
return;
|
|
}
|
|
return this.parentVNode;
|
|
}
|
|
children() {
|
|
return [];
|
|
}
|
|
attach(context) {}
|
|
detach(context) {}
|
|
update(old, context) {
|
|
return null;
|
|
}
|
|
attached(context) {}
|
|
detached(context) {}
|
|
updated(context) {}
|
|
}
|
|
function nodeMatchesSpec(node, spec) {
|
|
return node instanceof Element && spec.type === node.tagName.toLowerCase();
|
|
}
|
|
const refinedElements = new WeakMap();
|
|
function markElementAsRefined(element, vdom) {
|
|
let refined;
|
|
if (refinedElements.has(vdom)) {
|
|
refined = refinedElements.get(vdom);
|
|
} else {
|
|
refined = new WeakSet();
|
|
refinedElements.set(vdom, refined);
|
|
}
|
|
refined.add(element);
|
|
}
|
|
function isElementRefined(element, vdom) {
|
|
return refinedElements.has(vdom) && refinedElements.get(vdom).has(element);
|
|
}
|
|
class ElementVNode extends VNodeBase {
|
|
constructor(spec, parent) {
|
|
super(parent);
|
|
this.spec = spec;
|
|
}
|
|
matches(other) {
|
|
return (
|
|
other instanceof ElementVNode && this.spec.type === other.spec.type
|
|
);
|
|
}
|
|
key() {
|
|
return this.spec.props.key;
|
|
}
|
|
children() {
|
|
return [this.child];
|
|
}
|
|
getExistingElement(context) {
|
|
const parent = context.parent;
|
|
const existing = context.node;
|
|
let element;
|
|
if (nodeMatchesSpec(existing, this.spec)) {
|
|
element = existing;
|
|
} else if (
|
|
!isElementRefined(parent, context.vdom) &&
|
|
context.vdom.isDOMNodeCaptured(parent)
|
|
) {
|
|
const sibling = context.sibling;
|
|
const guess = sibling
|
|
? sibling.nextElementSibling
|
|
: parent.firstElementChild;
|
|
if (guess && !context.vdom.isDOMNodeCaptured(guess)) {
|
|
if (nodeMatchesSpec(guess, this.spec)) {
|
|
element = guess;
|
|
} else {
|
|
parent.removeChild(guess);
|
|
}
|
|
}
|
|
}
|
|
return element;
|
|
}
|
|
attach(context) {
|
|
let element;
|
|
const existing = this.getExistingElement(context);
|
|
if (existing) {
|
|
element = existing;
|
|
} else {
|
|
element = createElement(this.spec, context.parent);
|
|
markElementAsRefined(element, context.vdom);
|
|
}
|
|
syncAttrs(element, this.spec.props, null);
|
|
this.child = createDOMVNode(element, this.spec.children, this, false);
|
|
}
|
|
update(prev, context) {
|
|
const prevContext = context.vdom.getVNodeContext(prev);
|
|
const element = prevContext.node;
|
|
syncAttrs(element, this.spec.props, prev.spec.props);
|
|
this.child = createDOMVNode(element, this.spec.children, this, false);
|
|
}
|
|
attached(context) {
|
|
const { oncreate, onrender } = this.spec.props;
|
|
if (oncreate) {
|
|
oncreate(context.node);
|
|
}
|
|
if (onrender) {
|
|
onrender(context.node);
|
|
}
|
|
}
|
|
detached(context) {
|
|
const { onremove } = this.spec.props;
|
|
if (onremove) {
|
|
onremove(context.node);
|
|
}
|
|
}
|
|
updated(context) {
|
|
const { onupdate, onrender } = this.spec.props;
|
|
if (onupdate) {
|
|
onupdate(context.node);
|
|
}
|
|
if (onrender) {
|
|
onrender(context.node);
|
|
}
|
|
}
|
|
}
|
|
const symbols = {
|
|
CREATED: Symbol(),
|
|
REMOVED: Symbol(),
|
|
UPDATED: Symbol(),
|
|
RENDERED: Symbol(),
|
|
ACTIVE: Symbol(),
|
|
DEFAULTS_ASSIGNED: Symbol(),
|
|
};
|
|
const domPlugins = [
|
|
[PLUGINS_CREATE_ELEMENT, pluginsCreateElement],
|
|
[PLUGINS_SET_ATTRIBUTE, pluginsSetAttribute],
|
|
];
|
|
class ComponentVNode extends VNodeBase {
|
|
constructor(spec, parent) {
|
|
super(parent);
|
|
this.lock = false;
|
|
this.spec = spec;
|
|
this.prev = null;
|
|
this.store = {};
|
|
this.store[symbols.ACTIVE] = this;
|
|
}
|
|
matches(other) {
|
|
return (
|
|
other instanceof ComponentVNode && this.spec.type === other.spec.type
|
|
);
|
|
}
|
|
key() {
|
|
return this.spec.props.key;
|
|
}
|
|
children() {
|
|
return [this.child];
|
|
}
|
|
createContext(context) {
|
|
const { parent } = context;
|
|
const { spec, prev, store } = this;
|
|
return {
|
|
spec,
|
|
prev,
|
|
store,
|
|
get node() {
|
|
return context.node;
|
|
},
|
|
get nodes() {
|
|
return context.nodes;
|
|
},
|
|
parent,
|
|
onCreate: (fn) => (store[symbols.CREATED] = fn),
|
|
onUpdate: (fn) => (store[symbols.UPDATED] = fn),
|
|
onRemove: (fn) => (store[symbols.REMOVED] = fn),
|
|
onRender: (fn) => (store[symbols.RENDERED] = fn),
|
|
refresh: () => {
|
|
const activeVNode = store[symbols.ACTIVE];
|
|
activeVNode.refresh(context);
|
|
},
|
|
leave: () => context.vdom.LEAVE,
|
|
getStore: (defaults) => {
|
|
if (defaults && !store[symbols.DEFAULTS_ASSIGNED]) {
|
|
Object.entries(defaults).forEach(([prop, value]) => {
|
|
store[prop] = value;
|
|
});
|
|
store[symbols.DEFAULTS_ASSIGNED] = true;
|
|
}
|
|
return store;
|
|
},
|
|
};
|
|
}
|
|
unbox(context) {
|
|
const Component = this.spec.type;
|
|
const props = this.spec.props;
|
|
const children = this.spec.children;
|
|
this.lock = true;
|
|
const prevContext = ComponentVNode.context;
|
|
ComponentVNode.context = this.createContext(context);
|
|
let unboxed = null;
|
|
try {
|
|
unboxed = Component(props, ...children);
|
|
} finally {
|
|
ComponentVNode.context = prevContext;
|
|
this.lock = false;
|
|
}
|
|
return unboxed;
|
|
}
|
|
refresh(context) {
|
|
if (this.lock) {
|
|
throw new Error("Calling refresh during unboxing causes infinite loop");
|
|
}
|
|
this.prev = this.spec;
|
|
const latestContext = context.vdom.getVNodeContext(this);
|
|
const unboxed = this.unbox(latestContext);
|
|
if (unboxed === context.vdom.LEAVE) {
|
|
return;
|
|
}
|
|
const prevChild = this.child;
|
|
this.child = createVNode(unboxed, this);
|
|
context.vdom.execute(this.child, prevChild);
|
|
this.updated(context);
|
|
}
|
|
addPlugins() {
|
|
addComponentPlugins(this.spec.type, domPlugins);
|
|
}
|
|
deletePlugins() {
|
|
deleteComponentPlugins(this.spec.type, domPlugins);
|
|
}
|
|
attach(context) {
|
|
this.addPlugins();
|
|
const unboxed = this.unbox(context);
|
|
const childSpec = unboxed === context.vdom.LEAVE ? null : unboxed;
|
|
this.child = createVNode(childSpec, this);
|
|
}
|
|
update(prev, context) {
|
|
this.store = prev.store;
|
|
this.prev = prev.spec;
|
|
this.store[symbols.ACTIVE] = this;
|
|
const prevContext = context.vdom.getVNodeContext(prev);
|
|
this.addPlugins();
|
|
const unboxed = this.unbox(prevContext);
|
|
let result = null;
|
|
if (unboxed === context.vdom.LEAVE) {
|
|
result = unboxed;
|
|
this.child = prev.child;
|
|
context.vdom.adoptVNode(this.child, this);
|
|
} else {
|
|
this.child = createVNode(unboxed, this);
|
|
}
|
|
return result;
|
|
}
|
|
handle(event, context) {
|
|
const fn = this.store[event];
|
|
if (fn) {
|
|
const nodes = context.nodes.length === 0 ? [null] : context.nodes;
|
|
fn(...nodes);
|
|
}
|
|
}
|
|
attached(context) {
|
|
this.deletePlugins();
|
|
this.handle(symbols.CREATED, context);
|
|
this.handle(symbols.RENDERED, context);
|
|
}
|
|
detached(context) {
|
|
this.handle(symbols.REMOVED, context);
|
|
}
|
|
updated(context) {
|
|
this.deletePlugins();
|
|
this.handle(symbols.UPDATED, context);
|
|
this.handle(symbols.RENDERED, context);
|
|
}
|
|
}
|
|
ComponentVNode.context = null;
|
|
function getComponentContext() {
|
|
return ComponentVNode.context;
|
|
}
|
|
class TextVNode extends VNodeBase {
|
|
constructor(text, parent) {
|
|
super(parent);
|
|
this.text = text;
|
|
}
|
|
matches(other) {
|
|
return other instanceof TextVNode;
|
|
}
|
|
children() {
|
|
return [this.child];
|
|
}
|
|
getExistingNode(context) {
|
|
const { parent } = context;
|
|
let node;
|
|
if (context.node instanceof Text) {
|
|
node = context.node;
|
|
} else if (
|
|
!isElementRefined(parent, context.vdom) &&
|
|
context.vdom.isDOMNodeCaptured(parent)
|
|
) {
|
|
const sibling = context.sibling;
|
|
const guess = sibling ? sibling.nextSibling : parent.firstChild;
|
|
if (
|
|
guess &&
|
|
!context.vdom.isDOMNodeCaptured(guess) &&
|
|
guess instanceof Text
|
|
) {
|
|
node = guess;
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
attach(context) {
|
|
const existing = this.getExistingNode(context);
|
|
let node;
|
|
if (existing) {
|
|
node = existing;
|
|
node.textContent = this.text;
|
|
} else {
|
|
node = document.createTextNode(this.text);
|
|
}
|
|
this.child = createVNode(node, this);
|
|
}
|
|
update(prev, context) {
|
|
const prevContext = context.vdom.getVNodeContext(prev);
|
|
const { node } = prevContext;
|
|
if (this.text !== prev.text) {
|
|
node.textContent = this.text;
|
|
}
|
|
this.child = createVNode(node, this);
|
|
}
|
|
}
|
|
class InlineFunctionVNode extends VNodeBase {
|
|
constructor(fn, parent) {
|
|
super(parent);
|
|
this.fn = fn;
|
|
}
|
|
matches(other) {
|
|
return other instanceof InlineFunctionVNode;
|
|
}
|
|
children() {
|
|
return [this.child];
|
|
}
|
|
call(context) {
|
|
const fn = this.fn;
|
|
const inlineFnContext = {
|
|
parent: context.parent,
|
|
get node() {
|
|
return context.node;
|
|
},
|
|
get nodes() {
|
|
return context.nodes;
|
|
},
|
|
};
|
|
const result = fn(inlineFnContext);
|
|
this.child = createVNode(result, this);
|
|
}
|
|
attach(context) {
|
|
this.call(context);
|
|
}
|
|
update(prev, context) {
|
|
const prevContext = context.vdom.getVNodeContext(prev);
|
|
this.call(prevContext);
|
|
}
|
|
}
|
|
class NullVNode extends VNodeBase {
|
|
matches(other) {
|
|
return other instanceof NullVNode;
|
|
}
|
|
}
|
|
class DOMVNode extends VNodeBase {
|
|
constructor(node, childSpecs, parent, isNative) {
|
|
super(parent);
|
|
this.node = node;
|
|
this.childSpecs = childSpecs;
|
|
this.isNative = isNative;
|
|
}
|
|
matches(other) {
|
|
return other instanceof DOMVNode && this.node === other.node;
|
|
}
|
|
wrap() {
|
|
this.childVNodes = this.childSpecs.map((spec) => createVNode(spec, this));
|
|
}
|
|
insertNode(context) {
|
|
const { parent, sibling } = context;
|
|
const shouldInsert = !(
|
|
parent === this.node.parentElement &&
|
|
sibling === this.node.previousSibling
|
|
);
|
|
if (shouldInsert) {
|
|
const target = sibling ? sibling.nextSibling : parent.firstChild;
|
|
parent.insertBefore(this.node, target);
|
|
}
|
|
}
|
|
attach(context) {
|
|
this.wrap();
|
|
this.insertNode(context);
|
|
}
|
|
detach(context) {
|
|
context.parent.removeChild(this.node);
|
|
}
|
|
update(prev, context) {
|
|
this.wrap();
|
|
this.insertNode(context);
|
|
}
|
|
cleanupDOMChildren(context) {
|
|
const element = this.node;
|
|
for (let current = element.lastChild; current != null; ) {
|
|
if (context.vdom.isDOMNodeCaptured(current)) {
|
|
current = current.previousSibling;
|
|
} else {
|
|
const prev = current.previousSibling;
|
|
element.removeChild(current);
|
|
current = prev;
|
|
}
|
|
}
|
|
}
|
|
refine(context) {
|
|
if (!this.isNative) {
|
|
this.cleanupDOMChildren(context);
|
|
}
|
|
const element = this.node;
|
|
markElementAsRefined(element, context.vdom);
|
|
}
|
|
attached(context) {
|
|
const { node } = this;
|
|
if (
|
|
node instanceof Element &&
|
|
!isElementRefined(node, context.vdom) &&
|
|
context.vdom.isDOMNodeCaptured(node)
|
|
) {
|
|
this.refine(context);
|
|
}
|
|
}
|
|
children() {
|
|
return this.childVNodes;
|
|
}
|
|
}
|
|
function isDOMVNode(v) {
|
|
return v instanceof DOMVNode;
|
|
}
|
|
function createDOMVNode(node, childSpecs, parent, isNative) {
|
|
return new DOMVNode(node, childSpecs, parent, isNative);
|
|
}
|
|
class ArrayVNode extends VNodeBase {
|
|
constructor(items, key, parent) {
|
|
super(parent);
|
|
this.items = items;
|
|
this.id = key;
|
|
}
|
|
matches(other) {
|
|
return other instanceof ArrayVNode;
|
|
}
|
|
key() {
|
|
return this.id;
|
|
}
|
|
children() {
|
|
return this.childVNodes;
|
|
}
|
|
wrap() {
|
|
this.childVNodes = this.items.map((spec) => createVNode(spec, this));
|
|
}
|
|
attach() {
|
|
this.wrap();
|
|
}
|
|
update() {
|
|
this.wrap();
|
|
}
|
|
}
|
|
function createVNode(spec, parent) {
|
|
if (isNodeSpec(spec)) {
|
|
return new ElementVNode(spec, parent);
|
|
}
|
|
if (isComponentSpec(spec)) {
|
|
if (spec.type === Array) {
|
|
return new ArrayVNode(spec.children, spec.props.key, parent);
|
|
}
|
|
return new ComponentVNode(spec, parent);
|
|
}
|
|
if (typeof spec === "string") {
|
|
return new TextVNode(spec, parent);
|
|
}
|
|
if (spec == null) {
|
|
return new NullVNode(parent);
|
|
}
|
|
if (typeof spec === "function") {
|
|
return new InlineFunctionVNode(spec, parent);
|
|
}
|
|
if (spec instanceof Node) {
|
|
return createDOMVNode(spec, [], parent, true);
|
|
}
|
|
if (Array.isArray(spec)) {
|
|
return new ArrayVNode(spec, null, parent);
|
|
}
|
|
throw new Error("Unable to create virtual node for spec");
|
|
}
|
|
|
|
function createVDOM(rootNode) {
|
|
const contexts = new WeakMap();
|
|
const hubs = new WeakMap();
|
|
const parentNodes = new WeakMap();
|
|
const passingLinks = new WeakMap();
|
|
const linkedParents = new WeakSet();
|
|
const LEAVE = Symbol();
|
|
function execute$1(vnode, old) {
|
|
execute(vnode, old, vdom);
|
|
}
|
|
function creatVNodeContext(vnode) {
|
|
const parentNode = parentNodes.get(vnode);
|
|
contexts.set(vnode, {
|
|
parent: parentNode,
|
|
get node() {
|
|
const linked = passingLinks
|
|
.get(vnode)
|
|
.find((link) => link.node != null);
|
|
return linked ? linked.node : null;
|
|
},
|
|
get nodes() {
|
|
return passingLinks
|
|
.get(vnode)
|
|
.map((link) => link.node)
|
|
.filter((node) => node);
|
|
},
|
|
get sibling() {
|
|
if (parentNode === rootNode.parentElement) {
|
|
return passingLinks.get(vnode).first.node.previousSibling;
|
|
}
|
|
const hub = hubs.get(parentNode);
|
|
let current = passingLinks.get(vnode).first;
|
|
while ((current = hub.links.before(current))) {
|
|
if (current.node) {
|
|
return current.node;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
vdom,
|
|
});
|
|
}
|
|
function createRootVNodeLinks(vnode) {
|
|
const parentNode =
|
|
rootNode.parentElement || document.createDocumentFragment();
|
|
const node = rootNode;
|
|
const links = new LinkedList({
|
|
parentNode,
|
|
node,
|
|
});
|
|
passingLinks.set(vnode, links.copy());
|
|
parentNodes.set(vnode, parentNode);
|
|
hubs.set(parentNode, {
|
|
node: parentNode,
|
|
links,
|
|
});
|
|
}
|
|
function createVNodeLinks(vnode) {
|
|
const parent = vnode.parent();
|
|
const isBranch = linkedParents.has(parent);
|
|
const parentNode = isDOMVNode(parent)
|
|
? parent.node
|
|
: parentNodes.get(parent);
|
|
parentNodes.set(vnode, parentNode);
|
|
const vnodeLinks = new LinkedList();
|
|
passingLinks.set(vnode, vnodeLinks);
|
|
if (isBranch) {
|
|
const newLink = {
|
|
parentNode,
|
|
node: null,
|
|
};
|
|
let current = vnode;
|
|
do {
|
|
passingLinks.get(current).push(newLink);
|
|
current = current.parent();
|
|
} while (current && !isDOMVNode(current));
|
|
hubs.get(parentNode).links.push(newLink);
|
|
} else {
|
|
linkedParents.add(parent);
|
|
const links = isDOMVNode(parent)
|
|
? hubs.get(parentNode).links
|
|
: passingLinks.get(parent);
|
|
links.forEach((link) => vnodeLinks.push(link));
|
|
}
|
|
}
|
|
function connectDOMVNode(vnode) {
|
|
if (isDOMVNode(vnode)) {
|
|
const { node } = vnode;
|
|
hubs.set(node, {
|
|
node,
|
|
links: new LinkedList({
|
|
parentNode: node,
|
|
node: null,
|
|
}),
|
|
});
|
|
passingLinks.get(vnode).forEach((link) => (link.node = node));
|
|
}
|
|
}
|
|
function addVNode(vnode) {
|
|
const parent = vnode.parent();
|
|
if (parent == null) {
|
|
createRootVNodeLinks(vnode);
|
|
} else {
|
|
createVNodeLinks(vnode);
|
|
}
|
|
connectDOMVNode(vnode);
|
|
creatVNodeContext(vnode);
|
|
}
|
|
function getVNodeContext(vnode) {
|
|
return contexts.get(vnode);
|
|
}
|
|
function getAncestorsLinks(vnode) {
|
|
const parentNode = parentNodes.get(vnode);
|
|
const hub = hubs.get(parentNode);
|
|
const allLinks = [];
|
|
let current = vnode;
|
|
while ((current = current.parent()) && !isDOMVNode(current)) {
|
|
allLinks.push(passingLinks.get(current));
|
|
}
|
|
allLinks.push(hub.links);
|
|
return allLinks;
|
|
}
|
|
function replaceVNode(old, vnode) {
|
|
if (vnode.parent() == null) {
|
|
addVNode(vnode);
|
|
return;
|
|
}
|
|
const oldContext = contexts.get(old);
|
|
const { parent: parentNode } = oldContext;
|
|
parentNodes.set(vnode, parentNode);
|
|
const oldLinks = passingLinks.get(old);
|
|
const newLink = {
|
|
parentNode,
|
|
node: null,
|
|
};
|
|
getAncestorsLinks(vnode).forEach((links) => {
|
|
const nextLink = links.after(oldLinks.last);
|
|
oldLinks.forEach((link) => links.delete(link));
|
|
if (nextLink) {
|
|
links.insertBefore(newLink, nextLink);
|
|
} else {
|
|
links.push(newLink);
|
|
}
|
|
});
|
|
const vnodeLinks = new LinkedList(newLink);
|
|
passingLinks.set(vnode, vnodeLinks);
|
|
creatVNodeContext(vnode);
|
|
}
|
|
function adoptVNode(vnode, parent) {
|
|
const vnodeLinks = passingLinks.get(vnode);
|
|
const parentLinks = passingLinks.get(parent).copy();
|
|
vnode.parent(parent);
|
|
getAncestorsLinks(vnode).forEach((links) => {
|
|
vnodeLinks.forEach((link) =>
|
|
links.insertBefore(link, parentLinks.first)
|
|
);
|
|
parentLinks.forEach((link) => links.delete(link));
|
|
});
|
|
}
|
|
function isDOMNodeCaptured(node) {
|
|
return hubs.has(node) && node !== rootNode.parentElement;
|
|
}
|
|
const vdom = {
|
|
execute: execute$1,
|
|
addVNode,
|
|
getVNodeContext,
|
|
replaceVNode,
|
|
adoptVNode,
|
|
isDOMNodeCaptured,
|
|
LEAVE,
|
|
};
|
|
return vdom;
|
|
}
|
|
|
|
const roots = new WeakMap();
|
|
const vdoms = new WeakMap();
|
|
function realize(node, vnode) {
|
|
const old = roots.get(node) || null;
|
|
roots.set(node, vnode);
|
|
let vdom;
|
|
if (vdoms.has(node)) {
|
|
vdom = vdoms.get(node);
|
|
} else {
|
|
vdom = createVDOM(node);
|
|
vdoms.set(node, vdom);
|
|
}
|
|
vdom.execute(vnode, old);
|
|
return vdom.getVNodeContext(vnode);
|
|
}
|
|
function render$1(element, spec) {
|
|
const vnode = createDOMVNode(
|
|
element,
|
|
Array.isArray(spec) ? spec : [spec],
|
|
null,
|
|
false
|
|
);
|
|
realize(element, vnode);
|
|
return element;
|
|
}
|
|
function sync(node, spec) {
|
|
const vnode = createVNode(spec, null);
|
|
const context = realize(node, vnode);
|
|
const { nodes } = context;
|
|
if (nodes.length !== 1 || nodes[0] !== node) {
|
|
throw new Error("Spec does not match the node");
|
|
}
|
|
return nodes[0];
|
|
}
|
|
|
|
function normalize(attrsOrChild, ...otherChildren) {
|
|
const attrs =
|
|
isObject(attrsOrChild) && !isSpec(attrsOrChild) ? attrsOrChild : null;
|
|
const children =
|
|
attrs == null ? [attrsOrChild].concat(otherChildren) : otherChildren;
|
|
return { attrs, children };
|
|
}
|
|
function createTagFunction(tag) {
|
|
return (attrsOrChild, ...otherChildren) => {
|
|
const { attrs, children } = normalize(attrsOrChild, otherChildren);
|
|
return m(tag, attrs, children);
|
|
};
|
|
}
|
|
new Proxy(
|
|
{},
|
|
{
|
|
get: (_, tag) => {
|
|
return createTagFunction(tag);
|
|
},
|
|
}
|
|
);
|
|
|
|
const plugins = {
|
|
createElement: createPluginsAPI(PLUGINS_CREATE_ELEMENT),
|
|
setAttribute: createPluginsAPI(PLUGINS_SET_ATTRIBUTE),
|
|
};
|
|
|
|
const userAgent =
|
|
typeof navigator === "undefined"
|
|
? "some useragent"
|
|
: navigator.userAgent.toLowerCase();
|
|
const platform =
|
|
typeof navigator === "undefined"
|
|
? "some platform"
|
|
: navigator.platform.toLowerCase();
|
|
const isChromium =
|
|
userAgent.includes("chrome") || userAgent.includes("chromium");
|
|
const isThunderbird = userAgent.includes("thunderbird");
|
|
const isFirefox =
|
|
userAgent.includes("firefox") ||
|
|
userAgent.includes("librewolf") ||
|
|
isThunderbird;
|
|
const isVivaldi = userAgent.includes("vivaldi");
|
|
const isYaBrowser = userAgent.includes("yabrowser");
|
|
const isOpera = userAgent.includes("opr") || userAgent.includes("opera");
|
|
const isEdge = userAgent.includes("edg");
|
|
userAgent.includes("safari") && !isChromium;
|
|
const isWindows = platform.startsWith("win");
|
|
const isMacOS = platform.startsWith("mac");
|
|
const isMobile = userAgent.includes("mobile");
|
|
const chromiumVersion = (() => {
|
|
const m = userAgent.match(/chrom[e|ium]\/([^ ]+)/);
|
|
if (m && m[1]) {
|
|
return m[1];
|
|
}
|
|
return "";
|
|
})();
|
|
(() => {
|
|
try {
|
|
document.querySelector(":defined");
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
})();
|
|
function compareChromeVersions($a, $b) {
|
|
const a = $a.split(".").map((x) => parseInt(x));
|
|
const b = $b.split(".").map((x) => parseInt(x));
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (a[i] !== b[i]) {
|
|
return a[i] < b[i] ? -1 : 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
globalThis.chrome &&
|
|
globalThis.chrome.runtime &&
|
|
globalThis.chrome.runtime.getManifest &&
|
|
globalThis.chrome.runtime.getManifest().manifest_version === 3;
|
|
const isCSSColorSchemePropSupported = (() => {
|
|
if (typeof document === "undefined") {
|
|
return false;
|
|
}
|
|
const el = document.createElement("div");
|
|
el.setAttribute("style", "color-scheme: dark");
|
|
return el.style && el.style.colorScheme === "dark";
|
|
})();
|
|
|
|
const MessageType = {
|
|
UI_GET_DATA: "ui-get-data",
|
|
UI_SUBSCRIBE_TO_CHANGES: "ui-subscribe-to-changes",
|
|
UI_UNSUBSCRIBE_FROM_CHANGES: "ui-unsubscribe-from-changes",
|
|
UI_CHANGE_SETTINGS: "ui-change-settings",
|
|
UI_SET_THEME: "ui-set-theme",
|
|
UI_SET_SHORTCUT: "ui-set-shortcut",
|
|
UI_TOGGLE_ACTIVE_TAB: "ui-toggle-active-tab",
|
|
UI_MARK_NEWS_AS_READ: "ui-mark-news-as-read",
|
|
UI_MARK_NEWS_AS_DISPLAYED: "ui-mark-news-as-displayed",
|
|
UI_LOAD_CONFIG: "ui-load-config",
|
|
UI_APPLY_DEV_DYNAMIC_THEME_FIXES: "ui-apply-dev-dynamic-theme-fixes",
|
|
UI_RESET_DEV_DYNAMIC_THEME_FIXES: "ui-reset-dev-dynamic-theme-fixes",
|
|
UI_APPLY_DEV_INVERSION_FIXES: "ui-apply-dev-inversion-fixes",
|
|
UI_RESET_DEV_INVERSION_FIXES: "ui-reset-dev-inversion-fixes",
|
|
UI_APPLY_DEV_STATIC_THEMES: "ui-apply-dev-static-themes",
|
|
UI_RESET_DEV_STATIC_THEMES: "ui-reset-dev-static-themes",
|
|
UI_SAVE_FILE: "ui-save-file",
|
|
UI_REQUEST_EXPORT_CSS: "ui-request-export-css",
|
|
BG_CHANGES: "bg-changes",
|
|
BG_ADD_CSS_FILTER: "bg-add-css-filter",
|
|
BG_ADD_STATIC_THEME: "bg-add-static-theme",
|
|
BG_ADD_SVG_FILTER: "bg-add-svg-filter",
|
|
BG_ADD_DYNAMIC_THEME: "bg-add-dynamic-theme",
|
|
BG_EXPORT_CSS: "bg-export-css",
|
|
BG_UNSUPPORTED_SENDER: "bg-unsupported-sender",
|
|
BG_CLEAN_UP: "bg-clean-up",
|
|
BG_RELOAD: "bg-reload",
|
|
BG_FETCH_RESPONSE: "bg-fetch-response",
|
|
BG_UI_UPDATE: "bg-ui-update",
|
|
BG_CSS_UPDATE: "bg-css-update",
|
|
CS_COLOR_SCHEME_CHANGE: "cs-color-scheme-change",
|
|
CS_FRAME_CONNECT: "cs-frame-connect",
|
|
CS_FRAME_FORGET: "cs-frame-forget",
|
|
CS_FRAME_FREEZE: "cs-frame-freeze",
|
|
CS_FRAME_RESUME: "cs-frame-resume",
|
|
CS_EXPORT_CSS_RESPONSE: "cs-export-css-response",
|
|
CS_FETCH: "cs-fetch",
|
|
CS_DARK_THEME_DETECTED: "cs-dark-theme-detected",
|
|
CS_DARK_THEME_NOT_DETECTED: "cs-dark-theme-not-detected",
|
|
};
|
|
|
|
class Connector {
|
|
constructor() {
|
|
this.onChangesReceived = ({ type, data }) => {
|
|
if (type === MessageType.BG_CHANGES) {
|
|
this.changeSubscribers.forEach((callback) => callback(data));
|
|
}
|
|
};
|
|
this.changeSubscribers = new Set();
|
|
}
|
|
async sendRequest(type, data) {
|
|
return new Promise((resolve, reject) => {
|
|
chrome.runtime.sendMessage({ type, data }, ({ data, error }) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve(data);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
async firefoxSendRequestWithResponse(type, data) {
|
|
return new Promise((resolve, reject) => {
|
|
const dataPort = chrome.runtime.connect({ name: type });
|
|
dataPort.onDisconnect.addListener(() => reject());
|
|
dataPort.onMessage.addListener(({ data, error }) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else {
|
|
resolve(data);
|
|
}
|
|
dataPort.disconnect();
|
|
});
|
|
data && dataPort.postMessage({ data });
|
|
});
|
|
}
|
|
async getData() {
|
|
if (isFirefox) {
|
|
return await this.firefoxSendRequestWithResponse(
|
|
MessageType.UI_GET_DATA
|
|
);
|
|
}
|
|
return await this.sendRequest(MessageType.UI_GET_DATA);
|
|
}
|
|
subscribeToChanges(callback) {
|
|
this.changeSubscribers.add(callback);
|
|
if (this.changeSubscribers.size === 1) {
|
|
chrome.runtime.onMessage.addListener(this.onChangesReceived);
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_SUBSCRIBE_TO_CHANGES,
|
|
});
|
|
}
|
|
}
|
|
setShortcut(command, shortcut) {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_SET_SHORTCUT,
|
|
data: { command, shortcut },
|
|
});
|
|
}
|
|
changeSettings(settings) {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_CHANGE_SETTINGS,
|
|
data: settings,
|
|
});
|
|
}
|
|
setTheme(theme) {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_SET_THEME,
|
|
data: theme,
|
|
});
|
|
}
|
|
toggleActiveTab() {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_TOGGLE_ACTIVE_TAB,
|
|
data: {},
|
|
});
|
|
}
|
|
markNewsAsRead(ids) {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_MARK_NEWS_AS_READ,
|
|
data: ids,
|
|
});
|
|
}
|
|
markNewsAsDisplayed(ids) {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_MARK_NEWS_AS_DISPLAYED,
|
|
data: ids,
|
|
});
|
|
}
|
|
loadConfig(options) {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_LOAD_CONFIG,
|
|
data: options,
|
|
});
|
|
}
|
|
async applyDevDynamicThemeFixes(text) {
|
|
if (isFirefox) {
|
|
return await this.firefoxSendRequestWithResponse(
|
|
MessageType.UI_APPLY_DEV_DYNAMIC_THEME_FIXES,
|
|
text
|
|
);
|
|
}
|
|
return await this.sendRequest(
|
|
MessageType.UI_APPLY_DEV_DYNAMIC_THEME_FIXES,
|
|
text
|
|
);
|
|
}
|
|
resetDevDynamicThemeFixes() {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_RESET_DEV_DYNAMIC_THEME_FIXES,
|
|
});
|
|
}
|
|
async applyDevInversionFixes(text) {
|
|
if (isFirefox) {
|
|
return await this.firefoxSendRequestWithResponse(
|
|
MessageType.UI_APPLY_DEV_INVERSION_FIXES,
|
|
text
|
|
);
|
|
}
|
|
return await this.sendRequest(
|
|
MessageType.UI_APPLY_DEV_INVERSION_FIXES,
|
|
text
|
|
);
|
|
}
|
|
resetDevInversionFixes() {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_RESET_DEV_INVERSION_FIXES,
|
|
});
|
|
}
|
|
async applyDevStaticThemes(text) {
|
|
if (isFirefox) {
|
|
return await this.firefoxSendRequestWithResponse(
|
|
MessageType.UI_APPLY_DEV_STATIC_THEMES,
|
|
text
|
|
);
|
|
}
|
|
return await this.sendRequest(
|
|
MessageType.UI_APPLY_DEV_STATIC_THEMES,
|
|
text
|
|
);
|
|
}
|
|
resetDevStaticThemes() {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_RESET_DEV_STATIC_THEMES,
|
|
});
|
|
}
|
|
disconnect() {
|
|
if (this.changeSubscribers.size > 0) {
|
|
this.changeSubscribers.clear();
|
|
chrome.runtime.onMessage.removeListener(this.onChangesReceived);
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_UNSUBSCRIBE_FROM_CHANGES,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/* malevic@0.19.1 - Jan 8, 2022 */
|
|
|
|
function withForms(type) {
|
|
plugins.setAttribute.add(type, ({ element, attr, value }) => {
|
|
if (attr === "value" && element instanceof HTMLInputElement) {
|
|
const text = (element.value = value == null ? "" : value);
|
|
element.value = text;
|
|
return true;
|
|
}
|
|
return null;
|
|
});
|
|
return type;
|
|
}
|
|
|
|
/* malevic@0.19.1 - Jan 8, 2022 */
|
|
|
|
let currentUseStateFn = null;
|
|
function useState(initialState) {
|
|
if (!currentUseStateFn) {
|
|
throw new Error("`useState()` should be called inside a component");
|
|
}
|
|
return currentUseStateFn(initialState);
|
|
}
|
|
function withState(type) {
|
|
const Stateful = (props, ...children) => {
|
|
const context = getComponentContext();
|
|
const useState = (initial) => {
|
|
if (!context) {
|
|
return { state: initial, setState: null };
|
|
}
|
|
const { store, refresh } = context;
|
|
store.state = store.state || initial;
|
|
const setState = (newState) => {
|
|
if (lock) {
|
|
throw new Error(
|
|
"Setting state during unboxing causes infinite loop"
|
|
);
|
|
}
|
|
store.state = Object.assign(Object.assign({}, store.state), newState);
|
|
refresh();
|
|
};
|
|
return {
|
|
state: store.state,
|
|
setState,
|
|
};
|
|
};
|
|
let lock = true;
|
|
const prevUseStateFn = currentUseStateFn;
|
|
currentUseStateFn = useState;
|
|
let result;
|
|
try {
|
|
result = type(props, ...children);
|
|
} finally {
|
|
currentUseStateFn = prevUseStateFn;
|
|
lock = false;
|
|
}
|
|
return result;
|
|
};
|
|
return Stateful;
|
|
}
|
|
|
|
function classes(...args) {
|
|
const classes = [];
|
|
args
|
|
.filter((c) => Boolean(c))
|
|
.forEach((c) => {
|
|
if (typeof c === "string") {
|
|
classes.push(c);
|
|
} else if (typeof c === "object") {
|
|
classes.push(...Object.keys(c).filter((key) => Boolean(c[key])));
|
|
}
|
|
});
|
|
return classes.join(" ");
|
|
}
|
|
function compose(type, ...wrappers) {
|
|
return wrappers.reduce((t, w) => w(t), type);
|
|
}
|
|
function openFile(options, callback) {
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.style.display = "none";
|
|
if (options.extensions && options.extensions.length > 0) {
|
|
input.accept = options.extensions.map((ext) => `.${ext}`).join(",");
|
|
}
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => callback(reader.result);
|
|
input.onchange = () => {
|
|
if (input.files[0]) {
|
|
reader.readAsText(input.files[0]);
|
|
document.body.removeChild(input);
|
|
}
|
|
};
|
|
document.body.appendChild(input);
|
|
input.click();
|
|
}
|
|
function saveFile(name, content) {
|
|
if (isFirefox) {
|
|
const a = document.createElement("a");
|
|
a.href = URL.createObjectURL(new Blob([content]));
|
|
a.download = name;
|
|
a.click();
|
|
} else {
|
|
chrome.runtime.sendMessage({
|
|
type: MessageType.UI_SAVE_FILE,
|
|
data: { name, content },
|
|
});
|
|
}
|
|
}
|
|
function throttle$1(callback) {
|
|
let frameId = null;
|
|
return (...args) => {
|
|
if (!frameId) {
|
|
callback(...args);
|
|
frameId = requestAnimationFrame(() => (frameId = null));
|
|
}
|
|
};
|
|
}
|
|
function onSwipeStart(startEventObj, startHandler) {
|
|
const isTouchEvent =
|
|
typeof TouchEvent !== "undefined" && startEventObj instanceof TouchEvent;
|
|
const touchId = isTouchEvent
|
|
? startEventObj.changedTouches[0].identifier
|
|
: null;
|
|
const pointerMoveEvent = isTouchEvent ? "touchmove" : "mousemove";
|
|
const pointerUpEvent = isTouchEvent ? "touchend" : "mouseup";
|
|
if (!isTouchEvent) {
|
|
startEventObj.preventDefault();
|
|
}
|
|
function getSwipeEventObject(e) {
|
|
const { clientX, clientY } = isTouchEvent ? getTouch(e) : e;
|
|
return { clientX, clientY };
|
|
}
|
|
const startSE = getSwipeEventObject(startEventObj);
|
|
const { move: moveHandler, up: upHandler } = startHandler(
|
|
startSE,
|
|
startEventObj
|
|
);
|
|
function getTouch(e) {
|
|
return Array.from(e.changedTouches).find(
|
|
({ identifier: id }) => id === touchId
|
|
);
|
|
}
|
|
const onPointerMove = throttle$1((e) => {
|
|
const se = getSwipeEventObject(e);
|
|
moveHandler(se, e);
|
|
});
|
|
function onPointerUp(e) {
|
|
unsubscribe();
|
|
const se = getSwipeEventObject(e);
|
|
upHandler(se, e);
|
|
}
|
|
function unsubscribe() {
|
|
window.removeEventListener(pointerMoveEvent, onPointerMove);
|
|
window.removeEventListener(pointerUpEvent, onPointerUp);
|
|
}
|
|
window.addEventListener(pointerMoveEvent, onPointerMove, { passive: true });
|
|
window.addEventListener(pointerUpEvent, onPointerUp, { passive: true });
|
|
}
|
|
function createSwipeHandler(startHandler) {
|
|
return (e) => onSwipeStart(e, startHandler);
|
|
}
|
|
async function getFontList() {
|
|
return new Promise((resolve) => {
|
|
if (!chrome.fontSettings) {
|
|
resolve([
|
|
"serif",
|
|
"sans-serif",
|
|
"monospace",
|
|
"cursive",
|
|
"fantasy",
|
|
"system-ui",
|
|
]);
|
|
return;
|
|
}
|
|
chrome.fontSettings.getFontList((list) => {
|
|
const fonts = list.map((f) => f.fontId);
|
|
resolve(fonts);
|
|
});
|
|
});
|
|
}
|
|
|
|
function toArray(x) {
|
|
return Array.isArray(x) ? x : [x];
|
|
}
|
|
function mergeClass(cls, propsCls) {
|
|
const normalized = toArray(cls).concat(toArray(propsCls));
|
|
return classes(...normalized);
|
|
}
|
|
function omitAttrs(omit, attrs) {
|
|
const result = {};
|
|
Object.keys(attrs).forEach((key) => {
|
|
if (omit.indexOf(key) < 0) {
|
|
result[key] = attrs[key];
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
function isElementHidden(element) {
|
|
return element.offsetParent === null;
|
|
}
|
|
|
|
function Button(props, ...children) {
|
|
const cls = mergeClass("button", props.class);
|
|
const attrs = omitAttrs(["class"], props);
|
|
return m$1(
|
|
"button",
|
|
{ class: cls, ...attrs },
|
|
m$1("span", { class: "button__wrapper" }, ...children)
|
|
);
|
|
}
|
|
|
|
function CheckBox(props, ...children) {
|
|
const cls = mergeClass("checkbox", props.class);
|
|
const attrs = omitAttrs(["class", "checked", "onchange"], props);
|
|
const check = (domNode) => (domNode.checked = Boolean(props.checked));
|
|
return m$1(
|
|
"label",
|
|
{ class: cls, ...attrs },
|
|
m$1("input", {
|
|
class: "checkbox__input",
|
|
type: "checkbox",
|
|
checked: props.checked,
|
|
onchange: props.onchange,
|
|
onrender: check,
|
|
}),
|
|
m$1("span", { class: "checkbox__checkmark" }),
|
|
m$1("span", { class: "checkbox__content" }, children)
|
|
);
|
|
}
|
|
|
|
function hslToRGB({ h, s, l, a = 1 }) {
|
|
if (s === 0) {
|
|
const [r, b, g] = [l, l, l].map((x) => Math.round(x * 255));
|
|
return { r, g, b, a };
|
|
}
|
|
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
const m = l - c / 2;
|
|
const [r, g, b] = (
|
|
h < 60
|
|
? [c, x, 0]
|
|
: h < 120
|
|
? [x, c, 0]
|
|
: h < 180
|
|
? [0, c, x]
|
|
: h < 240
|
|
? [0, x, c]
|
|
: h < 300
|
|
? [x, 0, c]
|
|
: [c, 0, x]
|
|
).map((n) => Math.round((n + m) * 255));
|
|
return { r, g, b, a };
|
|
}
|
|
function rgbToHSL({ r: r255, g: g255, b: b255, a = 1 }) {
|
|
const r = r255 / 255;
|
|
const g = g255 / 255;
|
|
const b = b255 / 255;
|
|
const max = Math.max(r, g, b);
|
|
const min = Math.min(r, g, b);
|
|
const c = max - min;
|
|
const l = (max + min) / 2;
|
|
if (c === 0) {
|
|
return { h: 0, s: 0, l, a };
|
|
}
|
|
let h =
|
|
(max === r
|
|
? ((g - b) / c) % 6
|
|
: max === g
|
|
? (b - r) / c + 2
|
|
: (r - g) / c + 4) * 60;
|
|
if (h < 0) {
|
|
h += 360;
|
|
}
|
|
const s = c / (1 - Math.abs(2 * l - 1));
|
|
return { h, s, l, a };
|
|
}
|
|
function toFixed(n, digits = 0) {
|
|
const fixed = n.toFixed(digits);
|
|
if (digits === 0) {
|
|
return fixed;
|
|
}
|
|
const dot = fixed.indexOf(".");
|
|
if (dot >= 0) {
|
|
const zerosMatch = fixed.match(/0+$/);
|
|
if (zerosMatch) {
|
|
if (zerosMatch.index === dot + 1) {
|
|
return fixed.substring(0, dot);
|
|
}
|
|
return fixed.substring(0, zerosMatch.index);
|
|
}
|
|
}
|
|
return fixed;
|
|
}
|
|
function rgbToHexString({ r, g, b, a }) {
|
|
return `#${(a != null && a < 1 ? [r, g, b, Math.round(a * 255)] : [r, g, b])
|
|
.map((x) => {
|
|
return `${x < 16 ? "0" : ""}${x.toString(16)}`;
|
|
})
|
|
.join("")}`;
|
|
}
|
|
function hslToString(hsl) {
|
|
const { h, s, l, a } = hsl;
|
|
if (a != null && a < 1) {
|
|
return `hsla(${toFixed(h)}, ${toFixed(s * 100)}%, ${toFixed(
|
|
l * 100
|
|
)}%, ${toFixed(a, 2)})`;
|
|
}
|
|
return `hsl(${toFixed(h)}, ${toFixed(s * 100)}%, ${toFixed(l * 100)}%)`;
|
|
}
|
|
const rgbMatch = /^rgba?\([^\(\)]+\)$/;
|
|
const hslMatch = /^hsla?\([^\(\)]+\)$/;
|
|
const hexMatch = /^#[0-9a-f]+$/i;
|
|
function parse($color) {
|
|
const c = $color.trim().toLowerCase();
|
|
if (c.match(rgbMatch)) {
|
|
return parseRGB(c);
|
|
}
|
|
if (c.match(hslMatch)) {
|
|
return parseHSL(c);
|
|
}
|
|
if (c.match(hexMatch)) {
|
|
return parseHex(c);
|
|
}
|
|
if (knownColors.has(c)) {
|
|
return getColorByName(c);
|
|
}
|
|
if (systemColors.has(c)) {
|
|
return getSystemColor(c);
|
|
}
|
|
if ($color === "transparent") {
|
|
return { r: 0, g: 0, b: 0, a: 0 };
|
|
}
|
|
throw new Error(`Unable to parse ${$color}`);
|
|
}
|
|
function getNumbers($color) {
|
|
const numbers = [];
|
|
let prevPos = 0;
|
|
let isMining = false;
|
|
const startIndex = $color.indexOf("(");
|
|
$color = $color.substring(startIndex + 1, $color.length - 1);
|
|
for (let i = 0; i < $color.length; i++) {
|
|
const c = $color[i];
|
|
if ((c >= "0" && c <= "9") || c === "." || c === "+" || c === "-") {
|
|
isMining = true;
|
|
} else if (isMining && (c === " " || c === ",")) {
|
|
numbers.push($color.substring(prevPos, i));
|
|
isMining = false;
|
|
prevPos = i + 1;
|
|
} else if (!isMining) {
|
|
prevPos = i + 1;
|
|
}
|
|
}
|
|
if (isMining) {
|
|
numbers.push($color.substring(prevPos, $color.length));
|
|
}
|
|
return numbers;
|
|
}
|
|
function getNumbersFromString(str, range, units) {
|
|
const raw = getNumbers(str);
|
|
const unitsList = Object.entries(units);
|
|
const numbers = raw
|
|
.map((r) => r.trim())
|
|
.map((r, i) => {
|
|
let n;
|
|
const unit = unitsList.find(([u]) => r.endsWith(u));
|
|
if (unit) {
|
|
n =
|
|
(parseFloat(r.substring(0, r.length - unit[0].length)) / unit[1]) *
|
|
range[i];
|
|
} else {
|
|
n = parseFloat(r);
|
|
}
|
|
if (range[i] > 1) {
|
|
return Math.round(n);
|
|
}
|
|
return n;
|
|
});
|
|
return numbers;
|
|
}
|
|
const rgbRange = [255, 255, 255, 1];
|
|
const rgbUnits = { "%": 100 };
|
|
function parseRGB($rgb) {
|
|
const [r, g, b, a = 1] = getNumbersFromString($rgb, rgbRange, rgbUnits);
|
|
return { r, g, b, a };
|
|
}
|
|
const hslRange = [360, 1, 1, 1];
|
|
const hslUnits = { "%": 100, deg: 360, rad: 2 * Math.PI, turn: 1 };
|
|
function parseHSL($hsl) {
|
|
const [h, s, l, a = 1] = getNumbersFromString($hsl, hslRange, hslUnits);
|
|
return hslToRGB({ h, s, l, a });
|
|
}
|
|
function parseHex($hex) {
|
|
const h = $hex.substring(1);
|
|
switch (h.length) {
|
|
case 3:
|
|
case 4: {
|
|
const [r, g, b] = [0, 1, 2].map((i) => parseInt(`${h[i]}${h[i]}`, 16));
|
|
const a = h.length === 3 ? 1 : parseInt(`${h[3]}${h[3]}`, 16) / 255;
|
|
return { r, g, b, a };
|
|
}
|
|
case 6:
|
|
case 8: {
|
|
const [r, g, b] = [0, 2, 4].map((i) =>
|
|
parseInt(h.substring(i, i + 2), 16)
|
|
);
|
|
const a = h.length === 6 ? 1 : parseInt(h.substring(6, 8), 16) / 255;
|
|
return { r, g, b, a };
|
|
}
|
|
}
|
|
throw new Error(`Unable to parse ${$hex}`);
|
|
}
|
|
function getColorByName($color) {
|
|
const n = knownColors.get($color);
|
|
return {
|
|
r: (n >> 16) & 255,
|
|
g: (n >> 8) & 255,
|
|
b: (n >> 0) & 255,
|
|
a: 1,
|
|
};
|
|
}
|
|
function getSystemColor($color) {
|
|
const n = systemColors.get($color);
|
|
return {
|
|
r: (n >> 16) & 255,
|
|
g: (n >> 8) & 255,
|
|
b: (n >> 0) & 255,
|
|
a: 1,
|
|
};
|
|
}
|
|
const knownColors = new Map(
|
|
Object.entries({
|
|
aliceblue: 0xf0f8ff,
|
|
antiquewhite: 0xfaebd7,
|
|
aqua: 0x00ffff,
|
|
aquamarine: 0x7fffd4,
|
|
azure: 0xf0ffff,
|
|
beige: 0xf5f5dc,
|
|
bisque: 0xffe4c4,
|
|
black: 0x000000,
|
|
blanchedalmond: 0xffebcd,
|
|
blue: 0x0000ff,
|
|
blueviolet: 0x8a2be2,
|
|
brown: 0xa52a2a,
|
|
burlywood: 0xdeb887,
|
|
cadetblue: 0x5f9ea0,
|
|
chartreuse: 0x7fff00,
|
|
chocolate: 0xd2691e,
|
|
coral: 0xff7f50,
|
|
cornflowerblue: 0x6495ed,
|
|
cornsilk: 0xfff8dc,
|
|
crimson: 0xdc143c,
|
|
cyan: 0x00ffff,
|
|
darkblue: 0x00008b,
|
|
darkcyan: 0x008b8b,
|
|
darkgoldenrod: 0xb8860b,
|
|
darkgray: 0xa9a9a9,
|
|
darkgrey: 0xa9a9a9,
|
|
darkgreen: 0x006400,
|
|
darkkhaki: 0xbdb76b,
|
|
darkmagenta: 0x8b008b,
|
|
darkolivegreen: 0x556b2f,
|
|
darkorange: 0xff8c00,
|
|
darkorchid: 0x9932cc,
|
|
darkred: 0x8b0000,
|
|
darksalmon: 0xe9967a,
|
|
darkseagreen: 0x8fbc8f,
|
|
darkslateblue: 0x483d8b,
|
|
darkslategray: 0x2f4f4f,
|
|
darkslategrey: 0x2f4f4f,
|
|
darkturquoise: 0x00ced1,
|
|
darkviolet: 0x9400d3,
|
|
deeppink: 0xff1493,
|
|
deepskyblue: 0x00bfff,
|
|
dimgray: 0x696969,
|
|
dimgrey: 0x696969,
|
|
dodgerblue: 0x1e90ff,
|
|
firebrick: 0xb22222,
|
|
floralwhite: 0xfffaf0,
|
|
forestgreen: 0x228b22,
|
|
fuchsia: 0xff00ff,
|
|
gainsboro: 0xdcdcdc,
|
|
ghostwhite: 0xf8f8ff,
|
|
gold: 0xffd700,
|
|
goldenrod: 0xdaa520,
|
|
gray: 0x808080,
|
|
grey: 0x808080,
|
|
green: 0x008000,
|
|
greenyellow: 0xadff2f,
|
|
honeydew: 0xf0fff0,
|
|
hotpink: 0xff69b4,
|
|
indianred: 0xcd5c5c,
|
|
indigo: 0x4b0082,
|
|
ivory: 0xfffff0,
|
|
khaki: 0xf0e68c,
|
|
lavender: 0xe6e6fa,
|
|
lavenderblush: 0xfff0f5,
|
|
lawngreen: 0x7cfc00,
|
|
lemonchiffon: 0xfffacd,
|
|
lightblue: 0xadd8e6,
|
|
lightcoral: 0xf08080,
|
|
lightcyan: 0xe0ffff,
|
|
lightgoldenrodyellow: 0xfafad2,
|
|
lightgray: 0xd3d3d3,
|
|
lightgrey: 0xd3d3d3,
|
|
lightgreen: 0x90ee90,
|
|
lightpink: 0xffb6c1,
|
|
lightsalmon: 0xffa07a,
|
|
lightseagreen: 0x20b2aa,
|
|
lightskyblue: 0x87cefa,
|
|
lightslategray: 0x778899,
|
|
lightslategrey: 0x778899,
|
|
lightsteelblue: 0xb0c4de,
|
|
lightyellow: 0xffffe0,
|
|
lime: 0x00ff00,
|
|
limegreen: 0x32cd32,
|
|
linen: 0xfaf0e6,
|
|
magenta: 0xff00ff,
|
|
maroon: 0x800000,
|
|
mediumaquamarine: 0x66cdaa,
|
|
mediumblue: 0x0000cd,
|
|
mediumorchid: 0xba55d3,
|
|
mediumpurple: 0x9370db,
|
|
mediumseagreen: 0x3cb371,
|
|
mediumslateblue: 0x7b68ee,
|
|
mediumspringgreen: 0x00fa9a,
|
|
mediumturquoise: 0x48d1cc,
|
|
mediumvioletred: 0xc71585,
|
|
midnightblue: 0x191970,
|
|
mintcream: 0xf5fffa,
|
|
mistyrose: 0xffe4e1,
|
|
moccasin: 0xffe4b5,
|
|
navajowhite: 0xffdead,
|
|
navy: 0x000080,
|
|
oldlace: 0xfdf5e6,
|
|
olive: 0x808000,
|
|
olivedrab: 0x6b8e23,
|
|
orange: 0xffa500,
|
|
orangered: 0xff4500,
|
|
orchid: 0xda70d6,
|
|
palegoldenrod: 0xeee8aa,
|
|
palegreen: 0x98fb98,
|
|
paleturquoise: 0xafeeee,
|
|
palevioletred: 0xdb7093,
|
|
papayawhip: 0xffefd5,
|
|
peachpuff: 0xffdab9,
|
|
peru: 0xcd853f,
|
|
pink: 0xffc0cb,
|
|
plum: 0xdda0dd,
|
|
powderblue: 0xb0e0e6,
|
|
purple: 0x800080,
|
|
rebeccapurple: 0x663399,
|
|
red: 0xff0000,
|
|
rosybrown: 0xbc8f8f,
|
|
royalblue: 0x4169e1,
|
|
saddlebrown: 0x8b4513,
|
|
salmon: 0xfa8072,
|
|
sandybrown: 0xf4a460,
|
|
seagreen: 0x2e8b57,
|
|
seashell: 0xfff5ee,
|
|
sienna: 0xa0522d,
|
|
silver: 0xc0c0c0,
|
|
skyblue: 0x87ceeb,
|
|
slateblue: 0x6a5acd,
|
|
slategray: 0x708090,
|
|
slategrey: 0x708090,
|
|
snow: 0xfffafa,
|
|
springgreen: 0x00ff7f,
|
|
steelblue: 0x4682b4,
|
|
tan: 0xd2b48c,
|
|
teal: 0x008080,
|
|
thistle: 0xd8bfd8,
|
|
tomato: 0xff6347,
|
|
turquoise: 0x40e0d0,
|
|
violet: 0xee82ee,
|
|
wheat: 0xf5deb3,
|
|
white: 0xffffff,
|
|
whitesmoke: 0xf5f5f5,
|
|
yellow: 0xffff00,
|
|
yellowgreen: 0x9acd32,
|
|
})
|
|
);
|
|
const systemColors = new Map(
|
|
Object.entries({
|
|
ActiveBorder: 0x3b99fc,
|
|
ActiveCaption: 0x000000,
|
|
AppWorkspace: 0xaaaaaa,
|
|
Background: 0x6363ce,
|
|
ButtonFace: 0xffffff,
|
|
ButtonHighlight: 0xe9e9e9,
|
|
ButtonShadow: 0x9fa09f,
|
|
ButtonText: 0x000000,
|
|
CaptionText: 0x000000,
|
|
GrayText: 0x7f7f7f,
|
|
Highlight: 0xb2d7ff,
|
|
HighlightText: 0x000000,
|
|
InactiveBorder: 0xffffff,
|
|
InactiveCaption: 0xffffff,
|
|
InactiveCaptionText: 0x000000,
|
|
InfoBackground: 0xfbfcc5,
|
|
InfoText: 0x000000,
|
|
Menu: 0xf6f6f6,
|
|
MenuText: 0xffffff,
|
|
Scrollbar: 0xaaaaaa,
|
|
ThreeDDarkShadow: 0x000000,
|
|
ThreeDFace: 0xc0c0c0,
|
|
ThreeDHighlight: 0xffffff,
|
|
ThreeDLightShadow: 0xffffff,
|
|
ThreeDShadow: 0x000000,
|
|
Window: 0xececec,
|
|
WindowFrame: 0xaaaaaa,
|
|
WindowText: 0x000000,
|
|
"-webkit-focus-ring-color": 0xe59700,
|
|
}).map(([key, value]) => [key.toLowerCase(), value])
|
|
);
|
|
|
|
function TextBox(props) {
|
|
const cls = mergeClass("textbox", props.class);
|
|
const attrs = omitAttrs(["class", "type"], props);
|
|
return m$1("input", {
|
|
class: cls,
|
|
type: "text",
|
|
spellcheck: "false",
|
|
...attrs,
|
|
});
|
|
}
|
|
|
|
function scale(x, inLow, inHigh, outLow, outHigh) {
|
|
return ((x - inLow) * (outHigh - outLow)) / (inHigh - inLow) + outLow;
|
|
}
|
|
function clamp(x, min, max) {
|
|
return Math.min(max, Math.max(min, x));
|
|
}
|
|
|
|
const hsbPickerDefaults = {
|
|
wasPrevHidden: true,
|
|
hueCanvasRendered: false,
|
|
activeHSB: null,
|
|
activeChangeHandler: null,
|
|
hueTouchStartHandler: null,
|
|
sbTouchStartHandler: null,
|
|
};
|
|
function rgbToHSB({ r, g, b }) {
|
|
const min = Math.min(r, g, b);
|
|
const max = Math.max(r, g, b);
|
|
return {
|
|
h: rgbToHSL({ r, g, b }).h,
|
|
s: max === 0 ? 0 : 1 - min / max,
|
|
b: max / 255,
|
|
};
|
|
}
|
|
function hsbToRGB({ h: hue, s: sat, b: br }) {
|
|
let c;
|
|
if (hue < 60) {
|
|
c = [1, hue / 60, 0];
|
|
} else if (hue < 120) {
|
|
c = [(120 - hue) / 60, 1, 0];
|
|
} else if (hue < 180) {
|
|
c = [0, 1, (hue - 120) / 60];
|
|
} else if (hue < 240) {
|
|
c = [0, (240 - hue) / 60, 1];
|
|
} else if (hue < 300) {
|
|
c = [(hue - 240) / 60, 0, 1];
|
|
} else {
|
|
c = [1, 0, (360 - hue) / 60];
|
|
}
|
|
const max = Math.max(...c);
|
|
const [r, g, b] = c
|
|
.map((v) => v + (max - v) * (1 - sat))
|
|
.map((v) => v * br)
|
|
.map((v) => Math.round(v * 255));
|
|
return { r, g, b, a: 1 };
|
|
}
|
|
function hsbToString(hsb) {
|
|
const rgb = hsbToRGB(hsb);
|
|
return rgbToHexString(rgb);
|
|
}
|
|
function render(canvas, getPixel) {
|
|
const { width, height } = canvas;
|
|
const context = canvas.getContext("2d");
|
|
const imageData = context.getImageData(0, 0, width, height);
|
|
const d = imageData.data;
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const i = 4 * (y * width + x);
|
|
const c = getPixel(x, y);
|
|
for (let j = 0; j < 4; j++) {
|
|
d[i + j] = c[j];
|
|
}
|
|
}
|
|
}
|
|
context.putImageData(imageData, 0, 0);
|
|
}
|
|
function renderHue(canvas) {
|
|
const { height } = canvas;
|
|
render(canvas, (_, y) => {
|
|
const hue = scale(y, 0, height, 0, 360);
|
|
const { r, g, b } = hsbToRGB({ h: hue, s: 1, b: 1 });
|
|
return new Uint8ClampedArray([r, g, b, 255]);
|
|
});
|
|
}
|
|
function renderSB(hue, canvas) {
|
|
const { width, height } = canvas;
|
|
render(canvas, (x, y) => {
|
|
const sat = scale(x, 0, width - 1, 0, 1);
|
|
const br = scale(y, 0, height - 1, 1, 0);
|
|
const { r, g, b } = hsbToRGB({ h: hue, s: sat, b: br });
|
|
return new Uint8ClampedArray([r, g, b, 255]);
|
|
});
|
|
}
|
|
function HSBPicker(props) {
|
|
const context = getComponentContext();
|
|
const store = context.getStore(hsbPickerDefaults);
|
|
store.activeChangeHandler = props.onChange;
|
|
const prevColor = context.prev && context.prev.props.color;
|
|
const prevActiveColor = store.activeHSB
|
|
? hsbToString(store.activeHSB)
|
|
: null;
|
|
const didColorChange =
|
|
props.color !== prevColor && props.color !== prevActiveColor;
|
|
let activeHSB;
|
|
if (didColorChange) {
|
|
const rgb = parse(props.color);
|
|
activeHSB = rgbToHSB(rgb);
|
|
store.activeHSB = activeHSB;
|
|
} else {
|
|
activeHSB = store.activeHSB;
|
|
}
|
|
function onSBCanvasRender(canvas) {
|
|
if (isElementHidden(canvas)) {
|
|
return;
|
|
}
|
|
const hue = activeHSB.h;
|
|
const prevHue = prevColor && rgbToHSB(parse(prevColor)).h;
|
|
if (store.wasPrevHidden || hue !== prevHue) {
|
|
renderSB(hue, canvas);
|
|
}
|
|
store.wasPrevHidden = false;
|
|
}
|
|
function onHueCanvasRender(canvas) {
|
|
if (store.hueCanvasRendered || isElementHidden(canvas)) {
|
|
return;
|
|
}
|
|
store.hueCanvasRendered = true;
|
|
renderHue(canvas);
|
|
}
|
|
function createHSBSwipeHandler(getEventHSB) {
|
|
return createSwipeHandler((startEvt, startNativeEvt) => {
|
|
const rect = startNativeEvt.currentTarget.getBoundingClientRect();
|
|
function onPointerMove(e) {
|
|
store.activeHSB = getEventHSB({ ...e, rect });
|
|
props.onColorPreview(hsbToString(store.activeHSB));
|
|
context.refresh();
|
|
}
|
|
function onPointerUp(e) {
|
|
const hsb = getEventHSB({ ...e, rect });
|
|
store.activeHSB = hsb;
|
|
props.onChange(hsbToString(hsb));
|
|
}
|
|
store.activeHSB = getEventHSB({ ...startEvt, rect });
|
|
context.refresh();
|
|
return {
|
|
move: onPointerMove,
|
|
up: onPointerUp,
|
|
};
|
|
});
|
|
}
|
|
const onSBPointerDown = createHSBSwipeHandler(
|
|
({ clientX, clientY, rect }) => {
|
|
const sat = clamp((clientX - rect.left) / rect.width, 0, 1);
|
|
const br = clamp(1 - (clientY - rect.top) / rect.height, 0, 1);
|
|
return { ...activeHSB, s: sat, b: br };
|
|
}
|
|
);
|
|
const onHuePointerDown = createHSBSwipeHandler(({ clientY, rect }) => {
|
|
const hue = clamp((clientY - rect.top) / rect.height, 0, 1) * 360;
|
|
return { ...activeHSB, h: hue };
|
|
});
|
|
const hueCursorStyle = {
|
|
"background-color": hslToString({ h: activeHSB.h, s: 1, l: 0.5, a: 1 }),
|
|
left: "0%",
|
|
top: `${(activeHSB.h / 360) * 100}%`,
|
|
};
|
|
const sbCursorStyle = {
|
|
"background-color": rgbToHexString(hsbToRGB(activeHSB)),
|
|
left: `${activeHSB.s * 100}%`,
|
|
top: `${(1 - activeHSB.b) * 100}%`,
|
|
};
|
|
return m$1(
|
|
"span",
|
|
{ class: "hsb-picker" },
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: "hsb-picker__sb-container",
|
|
onmousedown: onSBPointerDown,
|
|
onupdate: (el) => {
|
|
if (store.sbTouchStartHandler) {
|
|
el.removeEventListener("touchstart", store.sbTouchStartHandler);
|
|
}
|
|
el.addEventListener("touchstart", onSBPointerDown, {
|
|
passive: true,
|
|
});
|
|
store.sbTouchStartHandler = onSBPointerDown;
|
|
},
|
|
},
|
|
m$1("canvas", {
|
|
class: "hsb-picker__sb-canvas",
|
|
onrender: onSBCanvasRender,
|
|
}),
|
|
m$1("span", { class: "hsb-picker__sb-cursor", style: sbCursorStyle })
|
|
),
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: "hsb-picker__hue-container",
|
|
onmousedown: onHuePointerDown,
|
|
onupdate: (el) => {
|
|
if (store.hueTouchStartHandler) {
|
|
el.removeEventListener("touchstart", store.hueTouchStartHandler);
|
|
}
|
|
el.addEventListener("touchstart", onHuePointerDown, {
|
|
passive: true,
|
|
});
|
|
store.hueTouchStartHandler = onHuePointerDown;
|
|
},
|
|
},
|
|
m$1("canvas", {
|
|
class: "hsb-picker__hue-canvas",
|
|
onrender: onHueCanvasRender,
|
|
}),
|
|
m$1("span", { class: "hsb-picker__hue-cursor", style: hueCursorStyle })
|
|
)
|
|
);
|
|
}
|
|
|
|
function isValidColor(color) {
|
|
try {
|
|
parse(color);
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
const colorPickerFocuses = new WeakMap();
|
|
function focusColorPicker(node) {
|
|
const focus = colorPickerFocuses.get(node);
|
|
focus();
|
|
}
|
|
function ColorPicker(props) {
|
|
const context = getComponentContext();
|
|
context.onRender((node) => colorPickerFocuses.set(node, focus));
|
|
const store = context.store;
|
|
const isColorValid = isValidColor(props.color);
|
|
function onColorPreview(previewColor) {
|
|
store.previewNode.style.backgroundColor = previewColor;
|
|
store.textBoxNode.value = previewColor;
|
|
store.textBoxNode.blur();
|
|
}
|
|
function onColorChange(rawValue) {
|
|
const value = rawValue.trim();
|
|
if (isValidColor(value)) {
|
|
props.onChange(value);
|
|
} else {
|
|
props.onChange(props.color);
|
|
}
|
|
}
|
|
function focus() {
|
|
if (store.isFocused) {
|
|
return;
|
|
}
|
|
store.isFocused = true;
|
|
context.refresh();
|
|
window.addEventListener("mousedown", onOuterClick);
|
|
}
|
|
function blur() {
|
|
if (!store.isFocused) {
|
|
return;
|
|
}
|
|
window.removeEventListener("mousedown", onOuterClick);
|
|
store.isFocused = false;
|
|
context.refresh();
|
|
}
|
|
function toggleFocus() {
|
|
if (store.isFocused) {
|
|
blur();
|
|
} else {
|
|
focus();
|
|
}
|
|
}
|
|
function onOuterClick(e) {
|
|
if (!e.composedPath().some((el) => el === context.node)) {
|
|
blur();
|
|
}
|
|
}
|
|
const textBox = m$1(TextBox, {
|
|
class: "color-picker__input",
|
|
onrender: (el) => {
|
|
store.textBoxNode = el;
|
|
store.textBoxNode.value = isColorValid ? props.color : "";
|
|
},
|
|
onkeypress: (e) => {
|
|
const input = e.target;
|
|
if (e.key === "Enter") {
|
|
const { value } = input;
|
|
onColorChange(value);
|
|
blur();
|
|
onColorPreview(value);
|
|
}
|
|
},
|
|
onfocus: focus,
|
|
});
|
|
const previewElement = m$1("span", {
|
|
class: "color-picker__preview",
|
|
onclick: toggleFocus,
|
|
onrender: (el) => {
|
|
store.previewNode = el;
|
|
el.style.backgroundColor = isColorValid ? props.color : "transparent";
|
|
},
|
|
});
|
|
const resetButton = props.canReset
|
|
? m$1("span", {
|
|
role: "button",
|
|
class: "color-picker__reset",
|
|
onclick: () => {
|
|
props.onReset();
|
|
blur();
|
|
},
|
|
})
|
|
: null;
|
|
const textBoxLine = m$1(
|
|
"span",
|
|
{ class: "color-picker__textbox-line" },
|
|
textBox,
|
|
previewElement,
|
|
resetButton
|
|
);
|
|
const hsbLine = isColorValid
|
|
? m$1(
|
|
"span",
|
|
{ class: "color-picker__hsb-line" },
|
|
m$1(HSBPicker, {
|
|
color: props.color,
|
|
onChange: onColorChange,
|
|
onColorPreview: onColorPreview,
|
|
})
|
|
)
|
|
: null;
|
|
return m$1(
|
|
"span",
|
|
{
|
|
class: [
|
|
"color-picker",
|
|
store.isFocused && "color-picker--focused",
|
|
props.class,
|
|
],
|
|
},
|
|
m$1("span", { class: "color-picker__wrapper" }, textBoxLine, hsbLine)
|
|
);
|
|
}
|
|
var ColorPicker$1 = Object.assign(ColorPicker, { focus: focusColorPicker });
|
|
|
|
function DropDown(props) {
|
|
const context = getComponentContext();
|
|
const store = context.store;
|
|
if (context.prev) {
|
|
const currOptions = props.options.map((o) => o.id);
|
|
const prevOptions = context.prev.props.options.map((o) => o.id);
|
|
if (
|
|
currOptions.length !== prevOptions.length ||
|
|
currOptions.some((o, i) => o !== prevOptions[i])
|
|
) {
|
|
store.isOpen = false;
|
|
}
|
|
}
|
|
function saveListNode(el) {
|
|
store.listNode = el;
|
|
}
|
|
function saveSelectedNode(el) {
|
|
store.selectedNode = el;
|
|
}
|
|
function onSelectedClick() {
|
|
store.isOpen = !store.isOpen;
|
|
context.refresh();
|
|
if (store.isOpen) {
|
|
const onOuterClick = (e) => {
|
|
window.removeEventListener("mousedown", onOuterClick, false);
|
|
const listRect = store.listNode.getBoundingClientRect();
|
|
const ex = e.clientX;
|
|
const ey = e.clientY;
|
|
if (
|
|
ex < listRect.left ||
|
|
ex > listRect.right ||
|
|
ey < listRect.top ||
|
|
ey > listRect.bottom
|
|
) {
|
|
store.isOpen = false;
|
|
context.refresh();
|
|
}
|
|
};
|
|
window.addEventListener("mousedown", onOuterClick, false);
|
|
}
|
|
}
|
|
function createListItem(value) {
|
|
return m$1(
|
|
"span",
|
|
{
|
|
class: {
|
|
dropdown__list__item: true,
|
|
"dropdown__list__item--selected": value.id === props.selected,
|
|
[props.class]: props.class != null,
|
|
},
|
|
onclick: () => {
|
|
store.isOpen = false;
|
|
context.refresh();
|
|
props.onChange(value.id);
|
|
},
|
|
},
|
|
value.content
|
|
);
|
|
}
|
|
const selectedContent = props.options.find(
|
|
(value) => value.id === props.selected
|
|
).content;
|
|
return m$1(
|
|
"span",
|
|
{
|
|
class: {
|
|
dropdown: true,
|
|
"dropdown--open": store.isOpen,
|
|
[props.class]: Boolean(props.class),
|
|
},
|
|
},
|
|
m$1(
|
|
"span",
|
|
{ class: "dropdown__list", oncreate: saveListNode },
|
|
props.options
|
|
.slice()
|
|
.sort((a, b) =>
|
|
a.id === props.selected ? -1 : b.id === props.selected ? 1 : 0
|
|
)
|
|
.map(createListItem)
|
|
),
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: "dropdown__selected",
|
|
oncreate: saveSelectedNode,
|
|
onclick: onSelectedClick,
|
|
},
|
|
m$1("span", { class: "dropdown__selected__text" }, selectedContent)
|
|
)
|
|
);
|
|
}
|
|
|
|
function ColorDropDown(props) {
|
|
const context = getComponentContext();
|
|
const store = context.store;
|
|
const labels = {
|
|
DEFAULT: "Default",
|
|
AUTO: "Auto",
|
|
CUSTOM: "Custom",
|
|
};
|
|
const dropDownOptions = [
|
|
props.hasDefaultOption
|
|
? { id: "default", content: labels.DEFAULT }
|
|
: null,
|
|
props.hasAutoOption ? { id: "auto", content: labels.AUTO } : null,
|
|
{ id: "custom", content: labels.CUSTOM },
|
|
].filter((v) => v);
|
|
const selectedDropDownValue =
|
|
props.value === ""
|
|
? "default"
|
|
: props.value === "auto"
|
|
? "auto"
|
|
: "custom";
|
|
function onDropDownChange(value) {
|
|
const result = {
|
|
default: "",
|
|
auto: "auto",
|
|
custom: props.colorSuggestion,
|
|
}[value];
|
|
props.onChange(result);
|
|
}
|
|
let isPickerVisible;
|
|
try {
|
|
parse(props.value);
|
|
isPickerVisible = true;
|
|
} catch (err) {
|
|
isPickerVisible = false;
|
|
}
|
|
const prevValue = context.prev ? context.prev.props.value : null;
|
|
const shouldFocusOnPicker =
|
|
props.value !== "" &&
|
|
props.value !== "auto" &&
|
|
prevValue != null &&
|
|
(prevValue === "" || prevValue === "auto");
|
|
function onRootRender(root) {
|
|
if (shouldFocusOnPicker) {
|
|
const pickerNode = root.querySelector(".color-dropdown__picker");
|
|
ColorPicker$1.focus(pickerNode);
|
|
}
|
|
}
|
|
return m$1(
|
|
"span",
|
|
{
|
|
class: {
|
|
"color-dropdown": true,
|
|
"color-dropdown--open": store.isOpen,
|
|
[props.class]: Boolean(props.class),
|
|
},
|
|
onrender: onRootRender,
|
|
},
|
|
m$1(DropDown, {
|
|
class: "color-dropdown__options",
|
|
options: dropDownOptions,
|
|
selected: selectedDropDownValue,
|
|
onChange: onDropDownChange,
|
|
}),
|
|
m$1(ColorPicker$1, {
|
|
class: {
|
|
"color-dropdown__picker": true,
|
|
"color-dropdown__picker--hidden": !isPickerVisible,
|
|
},
|
|
color: props.value,
|
|
onChange: props.onChange,
|
|
canReset: true,
|
|
onReset: props.onReset,
|
|
})
|
|
);
|
|
}
|
|
|
|
const DEFAULT_OVERLAY_KEY = Symbol();
|
|
const overlayNodes = new Map();
|
|
const clickListeners = new WeakMap();
|
|
function getOverlayDOMNode(key) {
|
|
if (key == null) {
|
|
key = DEFAULT_OVERLAY_KEY;
|
|
}
|
|
if (!overlayNodes.has(key)) {
|
|
const node = document.createElement("div");
|
|
node.classList.add("overlay");
|
|
node.addEventListener("click", (e) => {
|
|
if (clickListeners.has(node) && e.currentTarget === node) {
|
|
const listener = clickListeners.get(node);
|
|
listener();
|
|
}
|
|
});
|
|
overlayNodes.set(key, node);
|
|
}
|
|
return overlayNodes.get(key);
|
|
}
|
|
function Overlay(props) {
|
|
return getOverlayDOMNode(props.key);
|
|
}
|
|
function Portal(props, ...content) {
|
|
const context = getComponentContext();
|
|
context.onRender(() => {
|
|
const node = getOverlayDOMNode(props.key);
|
|
if (props.onOuterClick) {
|
|
clickListeners.set(node, props.onOuterClick);
|
|
} else {
|
|
clickListeners.delete(node);
|
|
}
|
|
render$1(node, content);
|
|
});
|
|
context.onRemove(() => {
|
|
const container = getOverlayDOMNode(props.key);
|
|
render$1(container, null);
|
|
});
|
|
return context.leave();
|
|
}
|
|
var Overlay$1 = Object.assign(Overlay, { Portal });
|
|
|
|
function MessageBox(props) {
|
|
return m$1(
|
|
Overlay$1.Portal,
|
|
{ key: props.portalKey, onOuterClick: props.onCancel },
|
|
m$1(
|
|
"div",
|
|
{ class: "message-box" },
|
|
m$1("label", { class: "message-box__caption" }, props.caption),
|
|
m$1(
|
|
"div",
|
|
{ class: "message-box__buttons" },
|
|
m$1(
|
|
Button,
|
|
{
|
|
class: "message-box__button message-box__button-ok",
|
|
onclick: props.onOK,
|
|
},
|
|
"OK"
|
|
),
|
|
m$1(
|
|
Button,
|
|
{
|
|
class: "message-box__button message-box__button-cancel",
|
|
onclick: props.onCancel,
|
|
},
|
|
"Cancel"
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
function MultiSwitch(props, ...children) {
|
|
return m$1(
|
|
"span",
|
|
{ class: ["multi-switch", props.class] },
|
|
m$1("span", {
|
|
class: "multi-switch__highlight",
|
|
style: {
|
|
left: `${
|
|
(props.options.indexOf(props.value) / props.options.length) * 100
|
|
}%`,
|
|
width: `${(1 / props.options.length) * 100}%`,
|
|
},
|
|
}),
|
|
props.options.map((option) =>
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: {
|
|
"multi-switch__option": true,
|
|
"multi-switch__option--selected": option === props.value,
|
|
},
|
|
onclick: () => option !== props.value && props.onChange(option),
|
|
},
|
|
option
|
|
)
|
|
),
|
|
...children
|
|
);
|
|
}
|
|
|
|
function ResetButton$1(props, ...content) {
|
|
return m$1(
|
|
Button,
|
|
{ class: ["nav-button", props.class], onclick: props.onClick },
|
|
m$1("span", { class: "nav-button__content" }, ...content)
|
|
);
|
|
}
|
|
|
|
function ResetButton(props, ...content) {
|
|
return m$1(
|
|
Button,
|
|
{ class: "reset-button", onclick: props.onClick },
|
|
m$1(
|
|
"span",
|
|
{ class: "reset-button__content" },
|
|
m$1("span", { class: "reset-button__icon" }),
|
|
...content
|
|
)
|
|
);
|
|
}
|
|
|
|
function VirtualScroll(props) {
|
|
if (props.items.length === 0) {
|
|
return props.root;
|
|
}
|
|
const { store } = getComponentContext();
|
|
function renderContent(root, scrollToIndex) {
|
|
if (root.clientWidth === 0) {
|
|
return;
|
|
}
|
|
if (store.itemHeight == null) {
|
|
const tempItem = {
|
|
...props.items[0],
|
|
props: {
|
|
...props.items[0].props,
|
|
oncreate: null,
|
|
onupdate: null,
|
|
onrender: null,
|
|
},
|
|
};
|
|
const tempNode = render$1(root, tempItem).firstElementChild;
|
|
store.itemHeight = tempNode.getBoundingClientRect().height;
|
|
}
|
|
const { itemHeight } = store;
|
|
const wrapper = render$1(
|
|
root,
|
|
m$1("div", {
|
|
style: {
|
|
flex: "none",
|
|
height: `${props.items.length * itemHeight}px`,
|
|
overflow: "hidden",
|
|
position: "relative",
|
|
},
|
|
})
|
|
).firstElementChild;
|
|
if (scrollToIndex >= 0) {
|
|
root.scrollTop = scrollToIndex * itemHeight;
|
|
}
|
|
const containerHeight =
|
|
document.documentElement.clientHeight -
|
|
root.getBoundingClientRect().top;
|
|
let focusedIndex = -1;
|
|
if (document.activeElement) {
|
|
let current = document.activeElement;
|
|
while (current && current.parentElement !== wrapper) {
|
|
current = current.parentElement;
|
|
}
|
|
if (current) {
|
|
focusedIndex = store.nodesIndices.get(current);
|
|
}
|
|
}
|
|
store.nodesIndices = store.nodesIndices || new WeakMap();
|
|
const saveNodeIndex = (node, index) =>
|
|
store.nodesIndices.set(node, index);
|
|
const items = props.items
|
|
.map((item, index) => {
|
|
return { item, index };
|
|
})
|
|
.filter(({ index }) => {
|
|
const eTop = index * itemHeight;
|
|
const eBottom = (index + 1) * itemHeight;
|
|
const rTop = root.scrollTop;
|
|
const rBottom = root.scrollTop + containerHeight;
|
|
const isTopBoundVisible = eTop >= rTop && eTop <= rBottom;
|
|
const isBottomBoundVisible = eBottom >= rTop && eBottom <= rBottom;
|
|
return (
|
|
isTopBoundVisible || isBottomBoundVisible || focusedIndex === index
|
|
);
|
|
})
|
|
.map(({ item, index }) =>
|
|
m$1(
|
|
"div",
|
|
{
|
|
key: index,
|
|
onrender: (node) => saveNodeIndex(node, index),
|
|
style: {
|
|
left: "0",
|
|
position: "absolute",
|
|
top: `${index * itemHeight}px`,
|
|
width: "100%",
|
|
},
|
|
},
|
|
item
|
|
)
|
|
);
|
|
render$1(wrapper, items);
|
|
}
|
|
let rootNode;
|
|
let prevScrollTop;
|
|
const rootDidMount = props.root.props.oncreate;
|
|
const rootDidUpdate = props.root.props.onupdate;
|
|
const rootDidRender = props.root.props.onrender;
|
|
return {
|
|
...props.root,
|
|
props: {
|
|
...props.root.props,
|
|
oncreate: rootDidMount,
|
|
onupdate: rootDidUpdate,
|
|
onrender: (node) => {
|
|
rootNode = node;
|
|
rootDidRender && rootDidRender(rootNode);
|
|
renderContent(
|
|
rootNode,
|
|
isNaN(props.scrollToIndex) ? -1 : props.scrollToIndex
|
|
);
|
|
},
|
|
onscroll: () => {
|
|
if (rootNode.scrollTop === prevScrollTop) {
|
|
return;
|
|
}
|
|
prevScrollTop = rootNode.scrollTop;
|
|
renderContent(rootNode, -1);
|
|
},
|
|
},
|
|
children: [],
|
|
};
|
|
}
|
|
|
|
function Select(props) {
|
|
const { state, setState } = useState({
|
|
isExpanded: false,
|
|
focusedIndex: null,
|
|
});
|
|
const values = Object.keys(props.options);
|
|
const { store } = getComponentContext();
|
|
const valueNodes = store.valueNodes || (store.valueNodes = new Map());
|
|
const nodesValues =
|
|
store.nodesValues || (store.nodesValues = new WeakMap());
|
|
function onRender(node) {
|
|
store.rootNode = node;
|
|
}
|
|
function onOuterClick(e) {
|
|
const r = store.rootNode.getBoundingClientRect();
|
|
if (
|
|
e.clientX < r.left ||
|
|
e.clientX > r.right ||
|
|
e.clientY < r.top ||
|
|
e.clientY > r.bottom
|
|
) {
|
|
window.removeEventListener("click", onOuterClick);
|
|
collapseList();
|
|
}
|
|
}
|
|
function onTextInput(e) {
|
|
const text = e.target.value.toLowerCase().trim();
|
|
expandList();
|
|
values.some((value) => {
|
|
if (value.toLowerCase().indexOf(text) === 0) {
|
|
scrollToValue(value);
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
function onKeyPress(e) {
|
|
const input = e.target;
|
|
if (e.key === "Enter") {
|
|
const value = input.value;
|
|
input.blur();
|
|
collapseList();
|
|
props.onChange(value);
|
|
}
|
|
}
|
|
function scrollToValue(value) {
|
|
setState({ focusedIndex: values.indexOf(value) });
|
|
}
|
|
function onExpandClick() {
|
|
if (state.isExpanded) {
|
|
collapseList();
|
|
} else {
|
|
expandList();
|
|
}
|
|
}
|
|
function expandList() {
|
|
setState({ isExpanded: true });
|
|
scrollToValue(props.value);
|
|
window.addEventListener("click", onOuterClick);
|
|
}
|
|
function collapseList() {
|
|
setState({ isExpanded: false });
|
|
}
|
|
function onSelectOption(e) {
|
|
let current = e.target;
|
|
while (current && !nodesValues.has(current)) {
|
|
current = current.parentElement;
|
|
}
|
|
if (current) {
|
|
const value = nodesValues.get(current);
|
|
props.onChange(value);
|
|
}
|
|
collapseList();
|
|
}
|
|
function saveValueNode(value, domNode) {
|
|
valueNodes.set(value, domNode);
|
|
nodesValues.set(domNode, value);
|
|
}
|
|
function removeValueNode(value) {
|
|
const el = valueNodes.get(value);
|
|
valueNodes.delete(value);
|
|
nodesValues.delete(el);
|
|
}
|
|
return m$1(
|
|
"span",
|
|
{
|
|
class: ["select", state.isExpanded && "select--expanded", props.class],
|
|
onrender: onRender,
|
|
},
|
|
m$1(
|
|
"span",
|
|
{ class: "select__line" },
|
|
m$1(TextBox, {
|
|
class: "select__textbox",
|
|
value: props.value,
|
|
oninput: onTextInput,
|
|
onkeypress: onKeyPress,
|
|
}),
|
|
m$1(
|
|
Button,
|
|
{ class: "select__expand", onclick: onExpandClick },
|
|
m$1("span", { class: "select__expand__icon" })
|
|
)
|
|
),
|
|
m$1(VirtualScroll, {
|
|
root: m$1("span", {
|
|
class: {
|
|
select__list: true,
|
|
"select__list--expanded": state.isExpanded,
|
|
"select__list--short": Object.keys(props.options).length <= 7,
|
|
},
|
|
onclick: onSelectOption,
|
|
}),
|
|
items: Object.entries(props.options).map(([value, content]) =>
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: "select__option",
|
|
data: value,
|
|
onrender: (domNode) => saveValueNode(value, domNode),
|
|
onremove: () => removeValueNode(value),
|
|
},
|
|
content
|
|
)
|
|
),
|
|
scrollToIndex: state.focusedIndex,
|
|
})
|
|
);
|
|
}
|
|
var Select$1 = withState(Select);
|
|
|
|
function ShortcutLink(props) {
|
|
const cls = mergeClass("shortcut", props.class);
|
|
const shortcut = props.shortcuts[props.commandName];
|
|
const shortcutMessage = props.textTemplate(shortcut);
|
|
let enteringShortcutInProgress = false;
|
|
function startEnteringShortcut(node) {
|
|
if (enteringShortcutInProgress) {
|
|
return;
|
|
}
|
|
enteringShortcutInProgress = true;
|
|
const initialText = node.textContent;
|
|
node.textContent = "...⌨";
|
|
function onKeyDown(e) {
|
|
e.preventDefault();
|
|
const ctrl = e.ctrlKey;
|
|
const alt = e.altKey;
|
|
const command = e.metaKey;
|
|
const shift = e.shiftKey;
|
|
let key = null;
|
|
if (e.code.startsWith("Key")) {
|
|
key = e.code.substring(3);
|
|
} else if (e.code.startsWith("Digit")) {
|
|
key = e.code.substring(5);
|
|
}
|
|
const shortcut = `${
|
|
ctrl ? "Ctrl+" : alt ? "Alt+" : command ? "Command+" : ""
|
|
}${shift ? "Shift+" : ""}${key ? key : ""}`;
|
|
node.textContent = shortcut;
|
|
if ((ctrl || alt || command || shift) && key) {
|
|
removeListeners();
|
|
props.onSetShortcut(shortcut);
|
|
node.blur();
|
|
setTimeout(() => {
|
|
enteringShortcutInProgress = false;
|
|
node.classList.remove("shortcut--edit");
|
|
node.textContent = props.textTemplate(shortcut);
|
|
}, 500);
|
|
}
|
|
}
|
|
function onBlur() {
|
|
removeListeners();
|
|
node.classList.remove("shortcut--edit");
|
|
node.textContent = initialText;
|
|
enteringShortcutInProgress = false;
|
|
}
|
|
function removeListeners() {
|
|
window.removeEventListener("keydown", onKeyDown, true);
|
|
window.removeEventListener("blur", onBlur, true);
|
|
}
|
|
window.addEventListener("keydown", onKeyDown, true);
|
|
window.addEventListener("blur", onBlur, true);
|
|
node.classList.add("shortcut--edit");
|
|
}
|
|
function onClick(e) {
|
|
e.preventDefault();
|
|
if (isFirefox) {
|
|
startEnteringShortcut(e.target);
|
|
return;
|
|
}
|
|
if (isEdge) {
|
|
chrome.tabs.create({
|
|
url: `edge://extensions/shortcuts`,
|
|
active: true,
|
|
});
|
|
return;
|
|
}
|
|
chrome.tabs.create({
|
|
url: `chrome://extensions/configureCommands#command-${chrome.runtime.id}-${props.commandName}`,
|
|
active: true,
|
|
});
|
|
}
|
|
function onRender(node) {
|
|
node.textContent = shortcutMessage;
|
|
}
|
|
return m$1("a", {
|
|
class: cls,
|
|
href: "#",
|
|
onclick: onClick,
|
|
oncreate: onRender,
|
|
});
|
|
}
|
|
|
|
function throttle(callback) {
|
|
let pending = false;
|
|
let frameId = null;
|
|
let lastArgs;
|
|
const throttled = (...args) => {
|
|
lastArgs = args;
|
|
if (frameId) {
|
|
pending = true;
|
|
} else {
|
|
callback(...lastArgs);
|
|
frameId = requestAnimationFrame(() => {
|
|
frameId = null;
|
|
if (pending) {
|
|
callback(...lastArgs);
|
|
pending = false;
|
|
}
|
|
});
|
|
}
|
|
};
|
|
const cancel = () => {
|
|
cancelAnimationFrame(frameId);
|
|
pending = false;
|
|
frameId = null;
|
|
};
|
|
return Object.assign(throttled, { cancel });
|
|
}
|
|
|
|
function stickToStep(x, step) {
|
|
const s = Math.round(x / step) * step;
|
|
const exp = Math.floor(Math.log10(step));
|
|
if (exp >= 0) {
|
|
const m = 10 ** exp;
|
|
return Math.round(s / m) * m;
|
|
}
|
|
const m = 10 ** -exp;
|
|
return Math.round(s * m) / m;
|
|
}
|
|
function Slider(props) {
|
|
const context = getComponentContext();
|
|
const store = context.store;
|
|
store.activeProps = props;
|
|
function onRootCreate(rootNode) {
|
|
rootNode.addEventListener("touchstart", onPointerDown, { passive: true });
|
|
}
|
|
function saveTrackNode(el) {
|
|
store.trackNode = el;
|
|
}
|
|
function getTrackNode() {
|
|
return store.trackNode;
|
|
}
|
|
function saveThumbNode(el) {
|
|
store.thumbNode = el;
|
|
}
|
|
function getThumbNode() {
|
|
return store.thumbNode;
|
|
}
|
|
function onPointerDown(startEvt) {
|
|
if (store.isActive) {
|
|
return;
|
|
}
|
|
const { getClientX, pointerMoveEvent, pointerUpEvent } = (() => {
|
|
const isTouchEvent =
|
|
typeof TouchEvent !== "undefined" && startEvt instanceof TouchEvent;
|
|
const touchId = isTouchEvent
|
|
? startEvt.changedTouches[0].identifier
|
|
: null;
|
|
function getTouch(e) {
|
|
const find = (touches) =>
|
|
Array.from(touches).find((t) => t.identifier === touchId);
|
|
return find(e.changedTouches) || find(e.touches);
|
|
}
|
|
function getClientX(e) {
|
|
const { clientX } = isTouchEvent ? getTouch(e) : e;
|
|
return clientX;
|
|
}
|
|
const pointerMoveEvent = isTouchEvent ? "touchmove" : "mousemove";
|
|
const pointerUpEvent = isTouchEvent ? "touchend" : "mouseup";
|
|
return { getClientX, pointerMoveEvent, pointerUpEvent };
|
|
})();
|
|
const dx = (() => {
|
|
const thumbRect = getThumbNode().getBoundingClientRect();
|
|
const startClientX = getClientX(startEvt);
|
|
const isThumbPressed =
|
|
startClientX >= thumbRect.left && startClientX <= thumbRect.right;
|
|
return isThumbPressed
|
|
? thumbRect.left + thumbRect.width / 2 - startClientX
|
|
: 0;
|
|
})();
|
|
function getEventValue(e) {
|
|
const { min, max } = store.activeProps;
|
|
const clientX = getClientX(e);
|
|
const rect = getTrackNode().getBoundingClientRect();
|
|
const scaled = scale(clientX + dx, rect.left, rect.right, min, max);
|
|
const clamped = clamp(scaled, min, max);
|
|
return clamped;
|
|
}
|
|
function onPointerMove(e) {
|
|
const value = getEventValue(e);
|
|
store.activeValue = value;
|
|
context.refresh();
|
|
}
|
|
function onPointerUp(e) {
|
|
unsubscribe();
|
|
const value = getEventValue(e);
|
|
store.isActive = false;
|
|
context.refresh();
|
|
store.activeValue = null;
|
|
const { onChange, step } = store.activeProps;
|
|
onChange(stickToStep(value, step));
|
|
}
|
|
function onKeyPress(e) {
|
|
if (e.key === "Escape") {
|
|
unsubscribe();
|
|
store.isActive = false;
|
|
store.activeValue = null;
|
|
context.refresh();
|
|
}
|
|
}
|
|
function subscribe() {
|
|
window.addEventListener(pointerMoveEvent, onPointerMove, {
|
|
passive: true,
|
|
});
|
|
window.addEventListener(pointerUpEvent, onPointerUp, { passive: true });
|
|
window.addEventListener("keypress", onKeyPress);
|
|
}
|
|
function unsubscribe() {
|
|
window.removeEventListener(pointerMoveEvent, onPointerMove);
|
|
window.removeEventListener(pointerUpEvent, onPointerUp);
|
|
window.removeEventListener("keypress", onKeyPress);
|
|
}
|
|
subscribe();
|
|
store.isActive = true;
|
|
store.activeValue = getEventValue(startEvt);
|
|
context.refresh();
|
|
}
|
|
function getValue() {
|
|
return store.activeValue == null ? props.value : store.activeValue;
|
|
}
|
|
const percent = scale(getValue(), props.min, props.max, 0, 100);
|
|
const thumbPositionStyleValue = `${percent}%`;
|
|
const shouldFlipText = percent > 75;
|
|
const formattedValue = props.formatValue(
|
|
stickToStep(getValue(), props.step)
|
|
);
|
|
function scaleWheelDelta(delta) {
|
|
return scale(delta, 0, -1000, 0, props.max - props.min);
|
|
}
|
|
const refreshOnWheel = throttle(() => {
|
|
store.activeValue = stickToStep(store.wheelValue, props.step);
|
|
store.wheelTimeoutId = setTimeout(() => {
|
|
const { onChange } = store.activeProps;
|
|
onChange(store.activeValue);
|
|
store.isActive = false;
|
|
store.activeValue = null;
|
|
store.wheelValue = null;
|
|
}, 400);
|
|
context.refresh();
|
|
});
|
|
function onWheel(event) {
|
|
if (store.wheelValue == null) {
|
|
store.wheelValue = getValue();
|
|
}
|
|
store.isActive = true;
|
|
clearTimeout(store.wheelTimeoutId);
|
|
event.preventDefault();
|
|
const accumulatedValue = store.wheelValue + scaleWheelDelta(event.deltaY);
|
|
store.wheelValue = clamp(accumulatedValue, props.min, props.max);
|
|
refreshOnWheel();
|
|
}
|
|
return m$1(
|
|
"span",
|
|
{
|
|
class: { slider: true, "slider--active": store.isActive },
|
|
oncreate: onRootCreate,
|
|
onmousedown: onPointerDown,
|
|
onwheel: onWheel,
|
|
},
|
|
m$1(
|
|
"span",
|
|
{ class: "slider__track", oncreate: saveTrackNode },
|
|
m$1("span", {
|
|
class: "slider__track__fill",
|
|
style: { width: thumbPositionStyleValue },
|
|
})
|
|
),
|
|
m$1(
|
|
"span",
|
|
{ class: "slider__thumb-wrapper" },
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: "slider__thumb",
|
|
oncreate: saveThumbNode,
|
|
style: { left: thumbPositionStyleValue },
|
|
},
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: {
|
|
slider__thumb__value: true,
|
|
"slider__thumb__value--flip": shouldFlipText,
|
|
},
|
|
},
|
|
formattedValue
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
function Tab({ isActive }, ...children) {
|
|
const tabCls = {
|
|
"tab-panel__tab": true,
|
|
"tab-panel__tab--active": isActive,
|
|
};
|
|
return m$1("div", { class: tabCls }, children);
|
|
}
|
|
|
|
function TabPanel(props) {
|
|
const tabsNames = Object.keys(props.tabs);
|
|
function isActiveTab(name, index) {
|
|
return name == null ? index === 0 : name === props.activeTab;
|
|
}
|
|
const buttons = tabsNames.map((name, i) => {
|
|
const btnCls = {
|
|
"tab-panel__button": true,
|
|
"tab-panel__button--active": isActiveTab(name, i),
|
|
};
|
|
return m$1(
|
|
Button,
|
|
{ class: btnCls, onclick: () => props.onSwitchTab(name) },
|
|
props.tabLabels[name]
|
|
);
|
|
});
|
|
const tabs = tabsNames.map((name, i) =>
|
|
m$1(Tab, { isActive: isActiveTab(name, i) }, props.tabs[name])
|
|
);
|
|
return m$1(
|
|
"div",
|
|
{ class: "tab-panel" },
|
|
m$1("div", { class: "tab-panel__tabs" }, tabs)
|
|
);
|
|
}
|
|
|
|
function TextList(props) {
|
|
const context = getComponentContext();
|
|
context.store.indices = context.store.indices || new WeakMap();
|
|
function onTextChange(e) {
|
|
const index = context.store.indices.get(e.target);
|
|
const values = props.values.slice();
|
|
const value = e.target.value.trim();
|
|
if (values.includes(value)) {
|
|
return;
|
|
}
|
|
if (!value) {
|
|
values.splice(index, 1);
|
|
} else if (index === values.length) {
|
|
values.push(value);
|
|
} else {
|
|
values.splice(index, 1, value);
|
|
}
|
|
props.onChange(values);
|
|
}
|
|
function createTextBox(text, index) {
|
|
const saveIndex = (node) => context.store.indices.set(node, index);
|
|
return m$1(TextBox, {
|
|
class: "text-list__textbox",
|
|
value: text,
|
|
onrender: saveIndex,
|
|
placeholder: props.placeholder,
|
|
});
|
|
}
|
|
let shouldFocus = false;
|
|
const node = context.node;
|
|
const prevProps = context.prev ? context.prev.props : null;
|
|
if (
|
|
node &&
|
|
props.isFocused &&
|
|
(!prevProps ||
|
|
!prevProps.isFocused ||
|
|
prevProps.values.length < props.values.length)
|
|
) {
|
|
focusLastNode();
|
|
}
|
|
function didMount(node) {
|
|
context.store.node = node;
|
|
if (props.isFocused) {
|
|
focusLastNode();
|
|
}
|
|
}
|
|
function focusLastNode() {
|
|
const node = context.store.node;
|
|
shouldFocus = true;
|
|
requestAnimationFrame(() => {
|
|
const inputs = node.querySelectorAll(".text-list__textbox");
|
|
const last = inputs.item(inputs.length - 1);
|
|
last.focus();
|
|
});
|
|
}
|
|
return m$1(VirtualScroll, {
|
|
root: m$1("div", {
|
|
class: ["text-list", props.class],
|
|
onchange: onTextChange,
|
|
oncreate: didMount,
|
|
}),
|
|
items: props.values
|
|
.map(createTextBox)
|
|
.concat(createTextBox("", props.values.length)),
|
|
scrollToIndex: shouldFocus ? props.values.length : -1,
|
|
});
|
|
}
|
|
|
|
function getLocalMessage(messageName) {
|
|
return chrome.i18n.getMessage(messageName);
|
|
}
|
|
function getUILanguage() {
|
|
let code;
|
|
if (
|
|
"i18n" in chrome &&
|
|
"getUILanguage" in chrome.i18n &&
|
|
typeof chrome.i18n.getUILanguage === "function"
|
|
) {
|
|
code = chrome.i18n.getUILanguage();
|
|
} else {
|
|
code = navigator.language.split("-")[0];
|
|
}
|
|
if (code.endsWith("-mac")) {
|
|
return code.substring(0, code.length - 4);
|
|
}
|
|
return code;
|
|
}
|
|
|
|
function parseTime($time) {
|
|
const parts = $time.split(":").slice(0, 2);
|
|
const lowercased = $time.trim().toLowerCase();
|
|
const isAM = lowercased.endsWith("am") || lowercased.endsWith("a.m.");
|
|
const isPM = lowercased.endsWith("pm") || lowercased.endsWith("p.m.");
|
|
let hours = parts.length > 0 ? parseInt(parts[0]) : 0;
|
|
if (isNaN(hours) || hours > 23) {
|
|
hours = 0;
|
|
}
|
|
if (isAM && hours === 12) {
|
|
hours = 0;
|
|
}
|
|
if (isPM && hours < 12) {
|
|
hours += 12;
|
|
}
|
|
let minutes = parts.length > 1 ? parseInt(parts[1]) : 0;
|
|
if (isNaN(minutes) || minutes > 59) {
|
|
minutes = 0;
|
|
}
|
|
return [hours, minutes];
|
|
}
|
|
function isInTimeIntervalUTC(time0, time1, timestamp) {
|
|
if (time1 < time0) {
|
|
return timestamp <= time1 || time0 <= timestamp;
|
|
}
|
|
return time0 < timestamp && timestamp < time1;
|
|
}
|
|
function getDuration(time) {
|
|
let duration = 0;
|
|
if (time.seconds) {
|
|
duration += time.seconds * 1000;
|
|
}
|
|
if (time.minutes) {
|
|
duration += time.minutes * 60 * 1000;
|
|
}
|
|
if (time.hours) {
|
|
duration += time.hours * 60 * 60 * 1000;
|
|
}
|
|
if (time.days) {
|
|
duration += time.days * 24 * 60 * 60 * 1000;
|
|
}
|
|
return duration;
|
|
}
|
|
function getSunsetSunriseUTCTime(latitude, longitude, date) {
|
|
const dec31 = Date.UTC(date.getUTCFullYear(), 0, 0, 0, 0, 0, 0);
|
|
const oneDay = getDuration({ days: 1 });
|
|
const dayOfYear = Math.floor((date.getTime() - dec31) / oneDay);
|
|
const zenith = 90.83333333333333;
|
|
const D2R = Math.PI / 180;
|
|
const R2D = 180 / Math.PI;
|
|
const lnHour = longitude / 15;
|
|
function getTime(isSunrise) {
|
|
const t = dayOfYear + ((isSunrise ? 6 : 18) - lnHour) / 24;
|
|
const M = 0.9856 * t - 3.289;
|
|
let L =
|
|
M + 1.916 * Math.sin(M * D2R) + 0.02 * Math.sin(2 * M * D2R) + 282.634;
|
|
if (L > 360) {
|
|
L -= 360;
|
|
} else if (L < 0) {
|
|
L += 360;
|
|
}
|
|
let RA = R2D * Math.atan(0.91764 * Math.tan(L * D2R));
|
|
if (RA > 360) {
|
|
RA -= 360;
|
|
} else if (RA < 0) {
|
|
RA += 360;
|
|
}
|
|
const Lquadrant = Math.floor(L / 90) * 90;
|
|
const RAquadrant = Math.floor(RA / 90) * 90;
|
|
RA += Lquadrant - RAquadrant;
|
|
RA /= 15;
|
|
const sinDec = 0.39782 * Math.sin(L * D2R);
|
|
const cosDec = Math.cos(Math.asin(sinDec));
|
|
const cosH =
|
|
(Math.cos(zenith * D2R) - sinDec * Math.sin(latitude * D2R)) /
|
|
(cosDec * Math.cos(latitude * D2R));
|
|
if (cosH > 1) {
|
|
return {
|
|
alwaysDay: false,
|
|
alwaysNight: true,
|
|
time: 0,
|
|
};
|
|
} else if (cosH < -1) {
|
|
return {
|
|
alwaysDay: true,
|
|
alwaysNight: false,
|
|
time: 0,
|
|
};
|
|
}
|
|
const H =
|
|
(isSunrise ? 360 - R2D * Math.acos(cosH) : R2D * Math.acos(cosH)) / 15;
|
|
const T = H + RA - 0.06571 * t - 6.622;
|
|
let UT = T - lnHour;
|
|
if (UT > 24) {
|
|
UT -= 24;
|
|
} else if (UT < 0) {
|
|
UT += 24;
|
|
}
|
|
return {
|
|
alwaysDay: false,
|
|
alwaysNight: false,
|
|
time: Math.round(UT * getDuration({ hours: 1 })),
|
|
};
|
|
}
|
|
const sunriseTime = getTime(true);
|
|
const sunsetTime = getTime(false);
|
|
if (sunriseTime.alwaysDay || sunsetTime.alwaysDay) {
|
|
return {
|
|
alwaysDay: true,
|
|
};
|
|
} else if (sunriseTime.alwaysNight || sunsetTime.alwaysNight) {
|
|
return {
|
|
alwaysNight: true,
|
|
};
|
|
}
|
|
return {
|
|
sunriseTime: sunriseTime.time,
|
|
sunsetTime: sunsetTime.time,
|
|
};
|
|
}
|
|
function isNightAtLocation(latitude, longitude, date = new Date()) {
|
|
const time = getSunsetSunriseUTCTime(latitude, longitude, date);
|
|
if (time.alwaysDay) {
|
|
return false;
|
|
} else if (time.alwaysNight) {
|
|
return true;
|
|
}
|
|
const sunriseTime = time.sunriseTime;
|
|
const sunsetTime = time.sunsetTime;
|
|
const currentTime =
|
|
date.getUTCHours() * getDuration({ hours: 1 }) +
|
|
date.getUTCMinutes() * getDuration({ minutes: 1 }) +
|
|
date.getUTCSeconds() * getDuration({ seconds: 1 }) +
|
|
date.getUTCMilliseconds();
|
|
return isInTimeIntervalUTC(sunsetTime, sunriseTime, currentTime);
|
|
}
|
|
|
|
const is12H = new Date().toLocaleTimeString(getUILanguage()).endsWith("M");
|
|
function toLocaleTime($time) {
|
|
const [hours, minutes] = parseTime($time);
|
|
const mm = `${minutes < 10 ? "0" : ""}${minutes}`;
|
|
if (is12H) {
|
|
const h = hours === 0 ? "12" : hours > 12 ? hours - 12 : hours;
|
|
return `${h}:${mm}${hours < 12 ? "AM" : "PM"}`;
|
|
}
|
|
return `${hours}:${mm}`;
|
|
}
|
|
function to24HTime($time) {
|
|
const [hours, minutes] = parseTime($time);
|
|
const mm = `${minutes < 10 ? "0" : ""}${minutes}`;
|
|
return `${hours}:${mm}`;
|
|
}
|
|
function TimeRangePicker(props) {
|
|
function onStartTimeChange($startTime) {
|
|
props.onChange([to24HTime($startTime), props.endTime]);
|
|
}
|
|
function onEndTimeChange($endTime) {
|
|
props.onChange([props.startTime, to24HTime($endTime)]);
|
|
}
|
|
function setStartTime(node) {
|
|
node.value = toLocaleTime(props.startTime);
|
|
}
|
|
function setEndTime(node) {
|
|
node.value = toLocaleTime(props.endTime);
|
|
}
|
|
return m$1(
|
|
"span",
|
|
{ class: "time-range-picker" },
|
|
m$1(TextBox, {
|
|
class: "time-range-picker__input time-range-picker__input--start",
|
|
placeholder: toLocaleTime("18:00"),
|
|
onrender: setStartTime,
|
|
onchange: (e) => onStartTimeChange(e.target.value),
|
|
onkeypress: (e) => {
|
|
if (e.key === "Enter") {
|
|
const input = e.target;
|
|
input.blur();
|
|
onStartTimeChange(input.value);
|
|
}
|
|
},
|
|
}),
|
|
m$1(TextBox, {
|
|
class: "time-range-picker__input time-range-picker__input--end",
|
|
placeholder: toLocaleTime("9:00"),
|
|
onrender: setEndTime,
|
|
onchange: (e) => onEndTimeChange(e.target.value),
|
|
onkeypress: (e) => {
|
|
if (e.key === "Enter") {
|
|
const input = e.target;
|
|
input.blur();
|
|
onEndTimeChange(input.value);
|
|
}
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
function Toggle(props) {
|
|
const { checked, onChange } = props;
|
|
const cls = ["toggle", checked ? "toggle--checked" : null, props.class];
|
|
const clsOn = {
|
|
toggle__btn: true,
|
|
toggle__on: true,
|
|
"toggle__btn--active": checked,
|
|
};
|
|
const clsOff = {
|
|
toggle__btn: true,
|
|
toggle__off: true,
|
|
"toggle__btn--active": !checked,
|
|
};
|
|
return m$1(
|
|
"span",
|
|
{ class: cls },
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: clsOn,
|
|
onclick: onChange ? () => !checked && onChange(true) : null,
|
|
},
|
|
props.labelOn
|
|
),
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: clsOff,
|
|
onclick: onChange ? () => checked && onChange(false) : null,
|
|
},
|
|
props.labelOff
|
|
)
|
|
);
|
|
}
|
|
|
|
function Track(props) {
|
|
const valueStyle = { width: `${props.value * 100}%` };
|
|
const isClickable = props.onChange != null;
|
|
function onMouseDown(e) {
|
|
const targetNode = e.currentTarget;
|
|
const valueNode = targetNode.firstElementChild;
|
|
targetNode.classList.add("track--active");
|
|
function getValue(clientX) {
|
|
const rect = targetNode.getBoundingClientRect();
|
|
return (clientX - rect.left) / rect.width;
|
|
}
|
|
function setWidth(value) {
|
|
valueNode.style.width = `${value * 100}%`;
|
|
}
|
|
function onMouseMove(e) {
|
|
const value = getValue(e.clientX);
|
|
setWidth(value);
|
|
}
|
|
function onMouseUp(e) {
|
|
const value = getValue(e.clientX);
|
|
props.onChange(value);
|
|
cleanup();
|
|
}
|
|
function onKeyPress(e) {
|
|
if (e.key === "Escape") {
|
|
setWidth(props.value);
|
|
cleanup();
|
|
}
|
|
}
|
|
function cleanup() {
|
|
window.removeEventListener("mousemove", onMouseMove);
|
|
window.removeEventListener("mouseup", onMouseUp);
|
|
window.removeEventListener("keypress", onKeyPress);
|
|
targetNode.classList.remove("track--active");
|
|
}
|
|
window.addEventListener("mousemove", onMouseMove);
|
|
window.addEventListener("mouseup", onMouseUp);
|
|
window.addEventListener("keypress", onKeyPress);
|
|
const value = getValue(e.clientX);
|
|
setWidth(value);
|
|
}
|
|
return m$1(
|
|
"span",
|
|
{
|
|
class: {
|
|
track: true,
|
|
"track--clickable": Boolean(props.onChange),
|
|
},
|
|
onmousedown: isClickable ? onMouseDown : null,
|
|
},
|
|
m$1("span", { class: "track__value", style: valueStyle }),
|
|
m$1("label", { class: "track__label" }, props.label)
|
|
);
|
|
}
|
|
|
|
function UpDown(props) {
|
|
const buttonDownCls = {
|
|
updown__button: true,
|
|
"updown__button--disabled": props.value === props.min,
|
|
};
|
|
const buttonUpCls = {
|
|
updown__button: true,
|
|
"updown__button--disabled": props.value === props.max,
|
|
};
|
|
function normalize(x) {
|
|
const s = Math.round(x / props.step) * props.step;
|
|
const exp = Math.floor(Math.log10(props.step));
|
|
if (exp >= 0) {
|
|
const m = 10 ** exp;
|
|
return Math.round(s / m) * m;
|
|
}
|
|
const m = 10 ** -exp;
|
|
return Math.round(s * m) / m;
|
|
}
|
|
function clamp(x) {
|
|
return Math.max(props.min, Math.min(props.max, x));
|
|
}
|
|
function onButtonDownClick() {
|
|
props.onChange(clamp(normalize(props.value - props.step)));
|
|
}
|
|
function onButtonUpClick() {
|
|
props.onChange(clamp(normalize(props.value + props.step)));
|
|
}
|
|
function onTrackValueChange(trackValue) {
|
|
props.onChange(
|
|
clamp(normalize(trackValue * (props.max - props.min) + props.min))
|
|
);
|
|
}
|
|
const trackValue = (props.value - props.min) / (props.max - props.min);
|
|
const valueText =
|
|
props.value === props.default
|
|
? getLocalMessage("off").toLocaleLowerCase()
|
|
: props.value > props.default
|
|
? `+${normalize(props.value - props.default)}`
|
|
: `-${normalize(props.default - props.value)}`;
|
|
return m$1(
|
|
"div",
|
|
{ class: "updown" },
|
|
m$1(
|
|
"div",
|
|
{ class: "updown__line" },
|
|
m$1(
|
|
Button,
|
|
{ class: buttonDownCls, onclick: onButtonDownClick },
|
|
m$1("span", { class: "updown__icon updown__icon-down" })
|
|
),
|
|
m$1(Track, {
|
|
value: trackValue,
|
|
label: props.name,
|
|
onChange: onTrackValueChange,
|
|
}),
|
|
m$1(
|
|
Button,
|
|
{ class: buttonUpCls, onclick: onButtonUpClick },
|
|
m$1("span", { class: "updown__icon updown__icon-up" })
|
|
)
|
|
),
|
|
m$1("label", { class: "updown__value-text" }, valueText)
|
|
);
|
|
}
|
|
|
|
function isIPV6(url) {
|
|
const openingBracketIndex = url.indexOf("[");
|
|
if (openingBracketIndex < 0) {
|
|
return false;
|
|
}
|
|
const queryIndex = url.indexOf("?");
|
|
if (queryIndex >= 0 && openingBracketIndex > queryIndex) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
const ipV6HostRegex = /\[.*?\](\:\d+)?/;
|
|
function compareIPV6(firstURL, secondURL) {
|
|
const firstHost = firstURL.match(ipV6HostRegex)[0];
|
|
const secondHost = secondURL.match(ipV6HostRegex)[0];
|
|
return firstHost === secondHost;
|
|
}
|
|
|
|
function getURLHostOrProtocol($url) {
|
|
const url = new URL($url);
|
|
if (url.host) {
|
|
return url.host;
|
|
} else if (url.protocol === "file:") {
|
|
return url.pathname;
|
|
}
|
|
return url.protocol;
|
|
}
|
|
function isURLInList(url, list) {
|
|
for (let i = 0; i < list.length; i++) {
|
|
if (isURLMatched(url, list[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function isURLMatched(url, urlTemplate) {
|
|
const isFirstIPV6 = isIPV6(url);
|
|
const isSecondIPV6 = isIPV6(urlTemplate);
|
|
if (isFirstIPV6 && isSecondIPV6) {
|
|
return compareIPV6(url, urlTemplate);
|
|
} else if (!isFirstIPV6 && !isSecondIPV6) {
|
|
const regex = createUrlRegex(urlTemplate);
|
|
return Boolean(url.match(regex));
|
|
}
|
|
return false;
|
|
}
|
|
function createUrlRegex(urlTemplate) {
|
|
urlTemplate = urlTemplate.trim();
|
|
const exactBeginning = urlTemplate[0] === "^";
|
|
const exactEnding = urlTemplate[urlTemplate.length - 1] === "$";
|
|
urlTemplate = urlTemplate
|
|
.replace(/^\^/, "")
|
|
.replace(/\$$/, "")
|
|
.replace(/^.*?\/{2,3}/, "")
|
|
.replace(/\?.*$/, "")
|
|
.replace(/\/$/, "");
|
|
let slashIndex;
|
|
let beforeSlash;
|
|
let afterSlash;
|
|
if ((slashIndex = urlTemplate.indexOf("/")) >= 0) {
|
|
beforeSlash = urlTemplate.substring(0, slashIndex);
|
|
afterSlash = urlTemplate.replace(/\$/g, "").substring(slashIndex);
|
|
} else {
|
|
beforeSlash = urlTemplate.replace(/\$/g, "");
|
|
}
|
|
let result = exactBeginning
|
|
? "^(.*?\\:\\/{2,3})?"
|
|
: "^(.*?\\:\\/{2,3})?([^/]*?\\.)?";
|
|
const hostParts = beforeSlash.split(".");
|
|
result += "(";
|
|
for (let i = 0; i < hostParts.length; i++) {
|
|
if (hostParts[i] === "*") {
|
|
hostParts[i] = "[^\\.\\/]+?";
|
|
}
|
|
}
|
|
result += hostParts.join("\\.");
|
|
result += ")";
|
|
if (afterSlash) {
|
|
result += "(";
|
|
result += afterSlash.replace("/", "\\/");
|
|
result += ")";
|
|
}
|
|
result += exactEnding ? "(\\/?(\\?[^/]*?)?)$" : "(\\/?.*?)$";
|
|
return new RegExp(result, "i");
|
|
}
|
|
function isPDF(url) {
|
|
if (url.includes(".pdf")) {
|
|
if (url.includes("?")) {
|
|
url = url.substring(0, url.lastIndexOf("?"));
|
|
}
|
|
if (url.includes("#")) {
|
|
url = url.substring(0, url.lastIndexOf("#"));
|
|
}
|
|
if (
|
|
(url.match(/(wikipedia|wikimedia).org/i) &&
|
|
url.match(
|
|
/(wikipedia|wikimedia)\.org\/.*\/[a-z]+\:[^\:\/]+\.pdf/i
|
|
)) ||
|
|
(url.match(/timetravel\.mementoweb\.org\/reconstruct/i) &&
|
|
url.match(/\.pdf$/i))
|
|
) {
|
|
return false;
|
|
}
|
|
if (url.endsWith(".pdf")) {
|
|
for (let i = url.length; i > 0; i--) {
|
|
if (url[i] === "=") {
|
|
return false;
|
|
} else if (url[i] === "/") {
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function isURLEnabled(
|
|
url,
|
|
userSettings,
|
|
{ isProtected, isInDarkList, isDarkThemeDetected }
|
|
) {
|
|
if (isProtected && !userSettings.enableForProtectedPages) {
|
|
return false;
|
|
}
|
|
if (isThunderbird) {
|
|
return true;
|
|
}
|
|
if (isPDF(url)) {
|
|
return userSettings.enableForPDF;
|
|
}
|
|
const isURLInUserList = isURLInList(url, userSettings.siteList);
|
|
const isURLInEnabledList = isURLInList(url, userSettings.siteListEnabled);
|
|
if (userSettings.applyToListedOnly) {
|
|
return isURLInEnabledList || isURLInUserList;
|
|
}
|
|
if (isURLInEnabledList) {
|
|
return true;
|
|
}
|
|
if (isInDarkList || (userSettings.detectDarkTheme && isDarkThemeDetected)) {
|
|
return false;
|
|
}
|
|
return !isURLInUserList;
|
|
}
|
|
|
|
function CustomSettingsToggle({ data, actions }) {
|
|
const tab = data.activeTab;
|
|
const host = getURLHostOrProtocol(tab.url);
|
|
const isCustom = data.settings.customThemes.some(({ url }) =>
|
|
isURLInList(tab.url, url)
|
|
);
|
|
const urlText = host
|
|
.split(".")
|
|
.reduce(
|
|
(elements, part, i) =>
|
|
elements.concat(m$1("wbr", null), `${i > 0 ? "." : ""}${part}`),
|
|
[]
|
|
);
|
|
}
|
|
|
|
function ModeToggle({ mode, onChange }) {
|
|
return m$1(
|
|
"div",
|
|
{ class: "mode-toggle" },
|
|
m$1(
|
|
"div",
|
|
{ class: "mode-toggle__line" },
|
|
m$1(
|
|
Button,
|
|
{
|
|
class: { "mode-toggle__button--active": mode === 1 },
|
|
onclick: () => onChange(1),
|
|
},
|
|
m$1("span", { class: "icon icon--dark-mode" })
|
|
),
|
|
m$1(Toggle, {
|
|
checked: mode === 1,
|
|
labelOn: getLocalMessage("dark"),
|
|
labelOff: getLocalMessage("light"),
|
|
onChange: (checked) => onChange(checked ? 1 : 0),
|
|
}),
|
|
m$1(
|
|
Button,
|
|
{
|
|
class: { "mode-toggle__button--active": mode === 0 },
|
|
onclick: () => onChange(0),
|
|
},
|
|
m$1("span", { class: "icon icon--light-mode" })
|
|
)
|
|
),
|
|
m$1("label", { class: "mode-toggle__label" }, getLocalMessage("mode"))
|
|
);
|
|
}
|
|
|
|
function FilterSettings({ data, actions }) {
|
|
const custom = data.settings.customThemes.find(({ url }) =>
|
|
isURLInList(data.activeTab.url, url)
|
|
);
|
|
const filterConfig = custom ? custom.theme : data.settings.theme;
|
|
function setConfig(config) {
|
|
if (custom) {
|
|
custom.theme = { ...custom.theme, ...config };
|
|
actions.changeSettings({ customThemes: data.settings.customThemes });
|
|
} else {
|
|
actions.setTheme(config);
|
|
}
|
|
}
|
|
const brightness = m$1(UpDown, {
|
|
value: filterConfig.brightness,
|
|
min: 50,
|
|
max: 150,
|
|
step: 5,
|
|
default: 100,
|
|
name: getLocalMessage("brightness"),
|
|
onChange: (value) => setConfig({ brightness: value }),
|
|
});
|
|
const contrast = m$1(UpDown, {
|
|
value: filterConfig.contrast,
|
|
min: 50,
|
|
max: 150,
|
|
step: 5,
|
|
default: 100,
|
|
name: getLocalMessage("contrast"),
|
|
onChange: (value) => setConfig({ contrast: value }),
|
|
});
|
|
const grayscale = m$1(UpDown, {
|
|
value: filterConfig.grayscale,
|
|
min: 0,
|
|
max: 100,
|
|
step: 5,
|
|
default: 0,
|
|
name: getLocalMessage("grayscale"),
|
|
onChange: (value) => setConfig({ grayscale: value }),
|
|
});
|
|
const sepia = m$1(UpDown, {
|
|
value: filterConfig.sepia,
|
|
min: 0,
|
|
max: 100,
|
|
step: 5,
|
|
default: 0,
|
|
name: getLocalMessage("sepia"),
|
|
onChange: (value) => setConfig({ sepia: value }),
|
|
});
|
|
return m$1(
|
|
"section",
|
|
{ class: "filter-settings" },
|
|
m$1(ModeToggle, {
|
|
mode: filterConfig.mode,
|
|
onChange: (mode) => setConfig({ mode }),
|
|
}),
|
|
brightness,
|
|
contrast,
|
|
sepia,
|
|
grayscale,
|
|
m$1(CustomSettingsToggle, { data: data, actions: actions })
|
|
);
|
|
}
|
|
|
|
function SunMoonIcon({ date, latitude, longitude }) {
|
|
if (latitude == null || longitude == null) {
|
|
return m$1(
|
|
"svg",
|
|
{ viewBox: "0 0 16 16" },
|
|
m$1(
|
|
"text",
|
|
{
|
|
fill: "white",
|
|
"font-size": "16",
|
|
"font-weight": "bold",
|
|
"text-anchor": "middle",
|
|
x: "8",
|
|
y: "14",
|
|
},
|
|
"?"
|
|
)
|
|
);
|
|
}
|
|
if (isNightAtLocation(latitude, longitude, date)) {
|
|
return m$1(
|
|
"svg",
|
|
{ viewBox: "0 0 16 16" },
|
|
m$1("path", {
|
|
fill: "white",
|
|
stroke: "none",
|
|
d: "M 6 3 Q 10 8 6 13 Q 12 13 12 8 Q 12 3 6 3",
|
|
})
|
|
);
|
|
}
|
|
return m$1(
|
|
"svg",
|
|
{ viewBox: "0 0 16 16" },
|
|
m$1("circle", {
|
|
fill: "white",
|
|
stroke: "none",
|
|
cx: "8",
|
|
cy: "8",
|
|
r: "3",
|
|
}),
|
|
m$1(
|
|
"g",
|
|
{
|
|
fill: "none",
|
|
stroke: "white",
|
|
"stroke-linecap": "round",
|
|
"stroke-width": "1.5",
|
|
},
|
|
...Array.from({ length: 8 }).map((_, i) => {
|
|
const cx = 8;
|
|
const cy = 8;
|
|
const angle = (i * Math.PI) / 4 + Math.PI / 8;
|
|
const pt = [5, 6].map((l) => [
|
|
cx + l * Math.cos(angle),
|
|
cy + l * Math.sin(angle),
|
|
]);
|
|
return m$1("line", {
|
|
x1: pt[0][0],
|
|
y1: pt[0][1],
|
|
x2: pt[1][0],
|
|
y2: pt[1][1],
|
|
});
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
function SystemIcon() {
|
|
return m$1(
|
|
"svg",
|
|
{ viewBox: "0 0 16 16" },
|
|
m$1("path", {
|
|
fill: "white",
|
|
stroke: "none",
|
|
d: "M3,3 h10 v7 h-3 v2 h1 v1 h-6 v-1 h1 v-2 h-3 z M4.5,4.5 v4 h7 v-4 z",
|
|
})
|
|
);
|
|
}
|
|
|
|
function WatchIcon({ hours, minutes }) {
|
|
const cx = 8;
|
|
const cy = 8.5;
|
|
const lenHour = 3;
|
|
const lenMinute = 4;
|
|
const clockR = 5.5;
|
|
const btnSize = 2;
|
|
const btnPad = 1.5;
|
|
const ah =
|
|
(((hours > 11 ? hours - 12 : hours) + minutes / 60) / 12) * Math.PI * 2;
|
|
const am = (minutes / 60) * Math.PI * 2;
|
|
const hx = cx + lenHour * Math.sin(ah);
|
|
const hy = cy - lenHour * Math.cos(ah);
|
|
const mx = cx + lenMinute * Math.sin(am);
|
|
const my = cy - lenMinute * Math.cos(am);
|
|
return m$1(
|
|
"svg",
|
|
{ viewBox: "0 0 16 16" },
|
|
m$1("circle", {
|
|
fill: "none",
|
|
stroke: "white",
|
|
"stroke-width": "1.5",
|
|
cx: cx,
|
|
cy: cy,
|
|
r: clockR,
|
|
}),
|
|
m$1("line", {
|
|
stroke: "white",
|
|
"stroke-width": "1.5",
|
|
x1: cx,
|
|
y1: cy,
|
|
x2: hx,
|
|
y2: hy,
|
|
}),
|
|
m$1("line", {
|
|
stroke: "white",
|
|
"stroke-width": "1.5",
|
|
opacity: "0.67",
|
|
x1: cx,
|
|
y1: cy,
|
|
x2: mx,
|
|
y2: my,
|
|
}),
|
|
[30, -30].map((angle) => {
|
|
return m$1("path", {
|
|
fill: "white",
|
|
transform: `rotate(${angle})`,
|
|
"transform-origin": `${cx} ${cy}`,
|
|
d: `M${cx - btnSize},${
|
|
cy - clockR - btnPad
|
|
} a${btnSize},${btnSize} 0 0 1 ${2 * btnSize},0 z`,
|
|
});
|
|
})
|
|
);
|
|
}
|
|
|
|
function CheckmarkIcon({ isChecked }) {
|
|
return m$1(
|
|
"svg",
|
|
{ viewBox: "0 0 8 8" },
|
|
m$1("path", {
|
|
d: isChecked
|
|
? "M1,4 l2,2 l4,-4 v1 l-4,4 l-2,-2 Z"
|
|
: "M2,2 l4,4 v1 l-4,-4 Z M2,6 l4,-4 v1 l-4,4 Z",
|
|
})
|
|
);
|
|
}
|
|
|
|
function SiteToggleButton({ data, actions }) {
|
|
const tab = data.activeTab;
|
|
function onSiteToggleClick() {
|
|
if (pdf) {
|
|
actions.changeSettings({ enableForPDF: !data.settings.enableForPDF });
|
|
} else {
|
|
actions.toggleActiveTab();
|
|
}
|
|
}
|
|
const pdf = isPDF(tab.url);
|
|
const toggleHasEffect =
|
|
data.settings.enableForProtectedPages ||
|
|
(!tab.isProtected && !pdf) ||
|
|
tab.isInjected;
|
|
const isSiteEnabled =
|
|
isURLEnabled(tab.url, data.settings, tab) && tab.isInjected;
|
|
const host = getURLHostOrProtocol(tab.url);
|
|
const urlText = host
|
|
.split(".")
|
|
.reduce(
|
|
(elements, part, i) =>
|
|
elements.concat(m$1("wbr", null), `${i > 0 ? "." : ""}${part}`),
|
|
[]
|
|
);
|
|
return m$1(
|
|
Button,
|
|
{
|
|
class: {
|
|
"site-toggle": true,
|
|
"site-toggle--active": isSiteEnabled,
|
|
"site-toggle--disabled": !toggleHasEffect || isThunderbird,
|
|
},
|
|
onclick: onSiteToggleClick,
|
|
},
|
|
m$1(
|
|
"span",
|
|
{ class: "site-toggle__mark" },
|
|
m$1(CheckmarkIcon, { isChecked: isSiteEnabled })
|
|
),
|
|
" ",
|
|
m$1("span", { class: "site-toggle__url" }, pdf ? "PDF" : urlText)
|
|
);
|
|
}
|
|
|
|
function MoreToggleSettings({ data, actions, isExpanded, onClose }) {
|
|
const isSystemAutomation = data.settings.automation === "system";
|
|
const locationSettings = data.settings.location;
|
|
const values = {
|
|
latitude: {
|
|
min: -90,
|
|
max: 90,
|
|
},
|
|
longitude: {
|
|
min: -180,
|
|
max: 180,
|
|
},
|
|
};
|
|
function getLocationString(location) {
|
|
if (location == null) {
|
|
return "";
|
|
}
|
|
return `${location}°`;
|
|
}
|
|
function locationChanged(inputElement, newValue, type) {
|
|
if (newValue.trim() === "") {
|
|
inputElement.value = "";
|
|
actions.changeSettings({
|
|
location: {
|
|
...locationSettings,
|
|
[type]: null,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
const min = values[type].min;
|
|
const max = values[type].max;
|
|
newValue = newValue.replace(",", ".").replace("°", "");
|
|
let num = Number(newValue);
|
|
if (isNaN(num)) {
|
|
num = 0;
|
|
} else if (num > max) {
|
|
num = max;
|
|
} else if (num < min) {
|
|
num = min;
|
|
}
|
|
inputElement.value = getLocationString(num);
|
|
actions.changeSettings({
|
|
location: {
|
|
...locationSettings,
|
|
[type]: num,
|
|
},
|
|
});
|
|
}
|
|
return m$1(
|
|
"div",
|
|
{
|
|
class: {
|
|
"header__app-toggle__more-settings": true,
|
|
"header__app-toggle__more-settings--expanded": isExpanded,
|
|
},
|
|
},
|
|
m$1(
|
|
"div",
|
|
{ class: "header__app-toggle__more-settings__top" },
|
|
m$1(
|
|
"span",
|
|
{ class: "header__app-toggle__more-settings__top__text" },
|
|
getLocalMessage("automation")
|
|
),
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: "header__app-toggle__more-settings__top__close",
|
|
role: "button",
|
|
onclick: onClose,
|
|
},
|
|
"\u2715"
|
|
)
|
|
),
|
|
m$1(
|
|
"div",
|
|
{ class: "header__app-toggle__more-settings__content" },
|
|
m$1(
|
|
"div",
|
|
{ class: "header__app-toggle__more-settings__line" },
|
|
m$1(CheckBox, {
|
|
checked: data.settings.automation === "time",
|
|
onchange: (e) =>
|
|
actions.changeSettings({
|
|
automation: e.target.checked ? "time" : "",
|
|
}),
|
|
}),
|
|
m$1(TimeRangePicker, {
|
|
startTime: data.settings.time.activation,
|
|
endTime: data.settings.time.deactivation,
|
|
onChange: ([start, end]) =>
|
|
actions.changeSettings({
|
|
time: { activation: start, deactivation: end },
|
|
}),
|
|
})
|
|
),
|
|
m$1(
|
|
"p",
|
|
{ class: "header__app-toggle__more-settings__description" },
|
|
getLocalMessage("set_active_hours")
|
|
),
|
|
m$1(
|
|
"div",
|
|
{
|
|
class:
|
|
"header__app-toggle__more-settings__line header__app-toggle__more-settings__location",
|
|
},
|
|
m$1(CheckBox, {
|
|
checked: data.settings.automation === "location",
|
|
onchange: (e) =>
|
|
actions.changeSettings({
|
|
automation: e.target.checked ? "location" : "",
|
|
}),
|
|
}),
|
|
m$1(TextBox, {
|
|
class: "header__app-toggle__more-settings__location__latitude",
|
|
placeholder: getLocalMessage("latitude"),
|
|
onchange: (e) =>
|
|
locationChanged(e.target, e.target.value, "latitude"),
|
|
oncreate: (node) =>
|
|
(node.value = getLocationString(locationSettings.latitude)),
|
|
onkeypress: (e) => {
|
|
if (e.key === "Enter") {
|
|
e.target.blur();
|
|
}
|
|
},
|
|
}),
|
|
m$1(TextBox, {
|
|
class: "header__app-toggle__more-settings__location__longitude",
|
|
placeholder: getLocalMessage("longitude"),
|
|
onchange: (e) =>
|
|
locationChanged(e.target, e.target.value, "longitude"),
|
|
oncreate: (node) =>
|
|
(node.value = getLocationString(locationSettings.longitude)),
|
|
onkeypress: (e) => {
|
|
if (e.key === "Enter") {
|
|
e.target.blur();
|
|
}
|
|
},
|
|
})
|
|
),
|
|
m$1(
|
|
"p",
|
|
{ class: "header__app-toggle__more-settings__location-description" },
|
|
getLocalMessage("set_location")
|
|
),
|
|
m$1(
|
|
"div",
|
|
{
|
|
class: [
|
|
"header__app-toggle__more-settings__line",
|
|
"header__app-toggle__more-settings__system-dark-mode",
|
|
],
|
|
},
|
|
m$1(CheckBox, {
|
|
class:
|
|
"header__app-toggle__more-settings__system-dark-mode__checkbox",
|
|
checked: isSystemAutomation,
|
|
onchange: (e) =>
|
|
actions.changeSettings({
|
|
automation: e.target.checked ? "system" : "",
|
|
}),
|
|
}),
|
|
m$1(
|
|
Button,
|
|
{
|
|
class: {
|
|
"header__app-toggle__more-settings__system-dark-mode__button": true,
|
|
"header__app-toggle__more-settings__system-dark-mode__button--active":
|
|
isSystemAutomation,
|
|
},
|
|
onclick: () =>
|
|
actions.changeSettings({
|
|
automation: isSystemAutomation ? "" : "system",
|
|
}),
|
|
},
|
|
getLocalMessage("system_dark_mode")
|
|
)
|
|
),
|
|
m$1(
|
|
"p",
|
|
{ class: "header__app-toggle__more-settings__description" },
|
|
getLocalMessage("system_dark_mode_description")
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
function multiline(...lines) {
|
|
return lines.join("\n");
|
|
}
|
|
function Header({ data, actions, onMoreToggleSettingsClick }) {
|
|
function toggleExtension(enabled) {
|
|
actions.changeSettings({
|
|
enabled,
|
|
automation: "",
|
|
});
|
|
}
|
|
const tab = data.activeTab;
|
|
const isAutomation = Boolean(data.settings.automation);
|
|
const isTimeAutomation = data.settings.automation === "time";
|
|
const isLocationAutomation = data.settings.automation === "location";
|
|
const now = new Date();
|
|
return m$1(
|
|
"header",
|
|
{ class: "header" },
|
|
|
|
m$1(
|
|
"div",
|
|
{ class: "header__control header__site-toggle" },
|
|
m$1(SiteToggleButton, { data: data, actions: actions }),
|
|
tab.isProtected || !tab.isInjected
|
|
? m$1(
|
|
"span",
|
|
{ class: "header__site-toggle__unable-text" },
|
|
getLocalMessage("page_protected")
|
|
)
|
|
: tab.isInDarkList
|
|
? m$1(
|
|
"span",
|
|
{ class: "header__site-toggle__unable-text" },
|
|
getLocalMessage("page_in_dark_list")
|
|
)
|
|
: m$1(ShortcutLink, {
|
|
commandName: "addSite",
|
|
shortcuts: data.shortcuts,
|
|
textTemplate: (hotkey) =>
|
|
getLocalMessage("setup_hotkey_toggle_site"),
|
|
onSetShortcut: (shortcut) =>
|
|
actions.setShortcut("addSite", shortcut),
|
|
})
|
|
),
|
|
|
|
m$1(
|
|
"div",
|
|
{ class: "header__control header__app-toggle" },
|
|
m$1(Toggle, {
|
|
checked: data.isEnabled,
|
|
labelOn: getLocalMessage("on"),
|
|
labelOff: getLocalMessage("off"),
|
|
onChange: toggleExtension,
|
|
}),
|
|
m$1(ShortcutLink, {
|
|
commandName: "toggle",
|
|
shortcuts: data.shortcuts,
|
|
textTemplate: (hotkey) =>
|
|
hotkey
|
|
? multiline(getLocalMessage("toggle_extension"), hotkey)
|
|
: getLocalMessage("setup_hotkey_toggle_extension"),
|
|
onSetShortcut: (shortcut) => actions.setShortcut("toggle", shortcut),
|
|
}),
|
|
m$1("span", {
|
|
class: "header__app-toggle__more-button",
|
|
onclick: onMoreToggleSettingsClick,
|
|
}),
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: {
|
|
"header__app-toggle__time": true,
|
|
"header__app-toggle__time--active": isAutomation,
|
|
},
|
|
},
|
|
isTimeAutomation
|
|
? m$1(WatchIcon, {
|
|
hours: now.getHours(),
|
|
minutes: now.getMinutes(),
|
|
})
|
|
: isLocationAutomation
|
|
? m$1(SunMoonIcon, {
|
|
date: now,
|
|
latitude: data.settings.location.latitude,
|
|
longitude: data.settings.location.longitude,
|
|
})
|
|
: m$1(SystemIcon, null)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
function Loader({ complete = false }) {
|
|
const context = getComponentContext();
|
|
const { state, setState } = useState({
|
|
finished: false,
|
|
errorOccured: false,
|
|
});
|
|
if (!state.errorOccured && !complete) {
|
|
context.store.loaderTimeoutID = setTimeout(() => {
|
|
setState({ errorOccured: true });
|
|
context.refresh();
|
|
}, 3000);
|
|
}
|
|
if (complete) {
|
|
clearTimeout(context.store.loaderTimeoutID);
|
|
}
|
|
const labelMessage = state.errorOccured
|
|
? "A unknown error has occured, the UI couldn't be loaded"
|
|
: getLocalMessage("loading_please_wait");
|
|
return m$1(
|
|
"div",
|
|
{
|
|
class: {
|
|
loader: true,
|
|
"loader--complete": complete,
|
|
"loader--transition-end": state.finished,
|
|
},
|
|
ontransitionend: () => setState({ finished: true }),
|
|
},
|
|
m$1(
|
|
"label",
|
|
{
|
|
class: {
|
|
loader__message: true,
|
|
loader__error: state.errorOccured,
|
|
},
|
|
},
|
|
labelMessage
|
|
)
|
|
);
|
|
}
|
|
var Loader$1 = withState(Loader);
|
|
|
|
function AutomationPage(props) {
|
|
const isSystemAutomation = props.data.settings.automation === "system";
|
|
const locationSettings = props.data.settings.location;
|
|
const values = {
|
|
latitude: {
|
|
min: -90,
|
|
max: 90,
|
|
},
|
|
longitude: {
|
|
min: -180,
|
|
max: 180,
|
|
},
|
|
};
|
|
function getLocationString(location) {
|
|
if (location == null) {
|
|
return "";
|
|
}
|
|
return `${location}°`;
|
|
}
|
|
function locationChanged(inputElement, newValue, type) {
|
|
if (newValue.trim() === "") {
|
|
inputElement.value = "";
|
|
props.actions.changeSettings({
|
|
location: {
|
|
...locationSettings,
|
|
[type]: null,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
const min = values[type].min;
|
|
const max = values[type].max;
|
|
newValue = newValue.replace(",", ".").replace("°", "");
|
|
let num = Number(newValue);
|
|
if (isNaN(num)) {
|
|
num = 0;
|
|
} else if (num > max) {
|
|
num = max;
|
|
} else if (num < min) {
|
|
num = min;
|
|
}
|
|
inputElement.value = getLocationString(num);
|
|
props.actions.changeSettings({
|
|
location: {
|
|
...locationSettings,
|
|
[type]: num,
|
|
},
|
|
});
|
|
}
|
|
return m$1(
|
|
"div",
|
|
{ class: "automation-page" },
|
|
m$1(
|
|
"div",
|
|
{ class: "automation-page__line" },
|
|
m$1(CheckBox, {
|
|
checked: props.data.settings.automation === "time",
|
|
onchange: (e) =>
|
|
props.actions.changeSettings({
|
|
automation: e.target.checked ? "time" : "",
|
|
}),
|
|
}),
|
|
m$1(TimeRangePicker, {
|
|
startTime: props.data.settings.time.activation,
|
|
endTime: props.data.settings.time.deactivation,
|
|
onChange: ([start, end]) =>
|
|
props.actions.changeSettings({
|
|
time: { activation: start, deactivation: end },
|
|
}),
|
|
})
|
|
),
|
|
m$1(
|
|
"p",
|
|
{ class: "automation-page__description" },
|
|
getLocalMessage("set_active_hours")
|
|
),
|
|
m$1(
|
|
"div",
|
|
{ class: "automation-page__line automation-page__location" },
|
|
m$1(CheckBox, {
|
|
checked: props.data.settings.automation === "location",
|
|
onchange: (e) =>
|
|
props.actions.changeSettings({
|
|
automation: e.target.checked ? "location" : "",
|
|
}),
|
|
}),
|
|
m$1(TextBox, {
|
|
class: "automation-page__location__latitude",
|
|
placeholder: getLocalMessage("latitude"),
|
|
onchange: (e) =>
|
|
locationChanged(e.target, e.target.value, "latitude"),
|
|
oncreate: (node) =>
|
|
(node.value = getLocationString(locationSettings.latitude)),
|
|
onkeypress: (e) => {
|
|
if (e.key === "Enter") {
|
|
e.target.blur();
|
|
}
|
|
},
|
|
}),
|
|
m$1(TextBox, {
|
|
class: "automation-page__location__longitude",
|
|
placeholder: getLocalMessage("longitude"),
|
|
onchange: (e) =>
|
|
locationChanged(e.target, e.target.value, "longitude"),
|
|
oncreate: (node) =>
|
|
(node.value = getLocationString(locationSettings.longitude)),
|
|
onkeypress: (e) => {
|
|
if (e.key === "Enter") {
|
|
e.target.blur();
|
|
}
|
|
},
|
|
})
|
|
),
|
|
m$1(
|
|
"p",
|
|
{ class: "automation-page__location-description" },
|
|
getLocalMessage("set_location")
|
|
),
|
|
m$1(
|
|
"div",
|
|
{
|
|
class: ["automation-page__line", "automation-page__system-dark-mode"],
|
|
},
|
|
m$1(CheckBox, {
|
|
class: "automation-page__system-dark-mode__checkbox",
|
|
checked: isSystemAutomation,
|
|
onchange: (e) =>
|
|
props.actions.changeSettings({
|
|
automation: e.target.checked ? "system" : "",
|
|
}),
|
|
}),
|
|
m$1(
|
|
Button,
|
|
{
|
|
class: {
|
|
"automation-page__system-dark-mode__button": true,
|
|
"automation-page__system-dark-mode__button--active":
|
|
isSystemAutomation,
|
|
},
|
|
onclick: () =>
|
|
props.actions.changeSettings({
|
|
automation: isSystemAutomation ? "" : "system",
|
|
}),
|
|
},
|
|
getLocalMessage("system_dark_mode")
|
|
)
|
|
),
|
|
m$1(
|
|
"p",
|
|
{ class: "automation-page__description" },
|
|
getLocalMessage("system_dark_mode_description")
|
|
),
|
|
m$1(DropDown, {
|
|
onChange: (selected) =>
|
|
props.actions.changeSettings({ automationBehaviour: selected }),
|
|
selected: props.data.settings.automationBehaviour,
|
|
options: [
|
|
{ id: "OnOff", content: "Toggle on/off" },
|
|
{ id: "Scheme", content: "Toggle dark/light" },
|
|
],
|
|
}),
|
|
m$1("p", { class: "automation-page__description" }, "Automation behavior")
|
|
);
|
|
}
|
|
|
|
function ControlGroup(props, control, description) {
|
|
return m$1(
|
|
"span",
|
|
{ class: ["control-group", props.class] },
|
|
control,
|
|
description
|
|
);
|
|
}
|
|
function Control(props, control) {
|
|
return m$1(
|
|
"span",
|
|
{ class: ["control-group__control", props.class] },
|
|
control
|
|
);
|
|
}
|
|
function Description(props, description) {
|
|
return m$1(
|
|
"span",
|
|
{ class: ["control-group__description", props.class] },
|
|
description
|
|
);
|
|
}
|
|
var ControlGroup$1 = Object.assign(ControlGroup, { Control, Description });
|
|
|
|
function AppSwitch(props) {
|
|
const isOn =
|
|
props.data.settings.enabled === true && !props.data.settings.automation;
|
|
const isOff =
|
|
props.data.settings.enabled === false && !props.data.settings.automation;
|
|
const isAutomation = Boolean(props.data.settings.automation);
|
|
const isTimeAutomation = props.data.settings.automation === "time";
|
|
const isLocationAutomation = props.data.settings.automation === "location";
|
|
const now = new Date();
|
|
const values = [getLocalMessage("on"), "Auto", getLocalMessage("off")];
|
|
const value = isOn ? values[0] : isOff ? values[2] : values[1];
|
|
function onSwitchChange(v) {
|
|
const index = values.indexOf(v);
|
|
if (index === 0) {
|
|
props.actions.changeSettings({
|
|
enabled: true,
|
|
automation: "",
|
|
});
|
|
} else if (index === 2) {
|
|
props.actions.changeSettings({
|
|
enabled: false,
|
|
automation: "",
|
|
});
|
|
} else if (index === 1) {
|
|
props.actions.changeSettings({
|
|
automation: "system",
|
|
});
|
|
}
|
|
}
|
|
const descriptionText = isOn
|
|
? "Extension is enabled"
|
|
: isOff
|
|
? "Extension is disabled"
|
|
: isTimeAutomation
|
|
? "Switches according to specified time"
|
|
: isLocationAutomation
|
|
? "Switched according to location"
|
|
: "Switches according to system dark mode";
|
|
const description = m$1(
|
|
"span",
|
|
{
|
|
class: {
|
|
"app-switch__description": true,
|
|
"app-switch__description--on": props.data.isEnabled,
|
|
"app-switch__description--off": !props.data.isEnabled,
|
|
},
|
|
},
|
|
descriptionText
|
|
);
|
|
return m$1(
|
|
ControlGroup$1,
|
|
{ class: "app-switch" },
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(
|
|
MultiSwitch,
|
|
{
|
|
class: "app-switch__control",
|
|
options: values,
|
|
value: value,
|
|
onChange: onSwitchChange,
|
|
},
|
|
m$1(
|
|
"span",
|
|
{
|
|
class: {
|
|
"app-switch__time": true,
|
|
"app-switch__time--active": isAutomation,
|
|
},
|
|
},
|
|
isTimeAutomation
|
|
? m$1(WatchIcon, {
|
|
hours: now.getHours(),
|
|
minutes: now.getMinutes(),
|
|
})
|
|
: isLocationAutomation
|
|
? m$1(SunMoonIcon, {
|
|
date: now,
|
|
latitude: props.data.settings.location.latitude,
|
|
longitude: props.data.settings.location.longitude,
|
|
})
|
|
: m$1(SystemIcon, null)
|
|
)
|
|
)
|
|
),
|
|
m$1(ControlGroup$1.Description, null, description)
|
|
);
|
|
}
|
|
|
|
function HelpGroup() {
|
|
return m$1(
|
|
ControlGroup$1,
|
|
null,
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(
|
|
"a",
|
|
{
|
|
class: "m-help-button",
|
|
href: getHelpURL(),
|
|
target: "_blank",
|
|
rel: "noopener noreferrer",
|
|
},
|
|
m$1("span", { class: "m-help-button__text" }, getLocalMessage("help"))
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
function SiteToggleGroup(props) {
|
|
const tab = props.data.activeTab;
|
|
const isPageEnabled = isURLEnabled(tab.url, props.data.settings, tab);
|
|
const { isDarkThemeDetected } = tab;
|
|
const descriptionText = isPDF(tab.url)
|
|
? isPageEnabled
|
|
? "Enabled for PDF files"
|
|
: "Disabled for PDF files"
|
|
: isDarkThemeDetected
|
|
? "Dark theme detected on page"
|
|
: isPageEnabled
|
|
? "Enabled for current website"
|
|
: "Disabled for current website";
|
|
const description = m$1(
|
|
"span",
|
|
{
|
|
class: {
|
|
"site-toggle-group__description": true,
|
|
"site-toggle-group__description--on": isPageEnabled,
|
|
"site-toggle-group__description--off": !isPageEnabled,
|
|
},
|
|
},
|
|
descriptionText
|
|
);
|
|
return m$1(
|
|
ControlGroup$1,
|
|
{ class: "site-toggle-group" },
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
{ class: "site-toggle-group__control" },
|
|
m$1(SiteToggleButton, { ...props })
|
|
),
|
|
m$1(ControlGroup$1.Description, null, description)
|
|
);
|
|
}
|
|
|
|
function ThemeControl(props, controls) {
|
|
return m$1(
|
|
"span",
|
|
{ class: "theme-control" },
|
|
m$1("label", { class: "theme-control__label" }, props.label),
|
|
controls
|
|
);
|
|
}
|
|
|
|
function BackgroundColorEditor(props) {
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Background" },
|
|
m$1(ColorPicker$1, {
|
|
color: props.value,
|
|
onChange: props.onChange,
|
|
canReset: props.canReset,
|
|
onReset: props.onReset,
|
|
})
|
|
);
|
|
}
|
|
|
|
function formatPercent(v) {
|
|
return `${v}%`;
|
|
}
|
|
|
|
function Brightness(props) {
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: getLocalMessage("brightness") },
|
|
m$1(Slider, {
|
|
value: props.value,
|
|
min: 50,
|
|
max: 150,
|
|
step: 1,
|
|
formatValue: formatPercent,
|
|
onChange: props.onChange,
|
|
})
|
|
);
|
|
}
|
|
|
|
function Contrast(props) {
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: getLocalMessage("contrast") },
|
|
m$1(Slider, {
|
|
value: props.value,
|
|
min: 50,
|
|
max: 150,
|
|
step: 1,
|
|
formatValue: formatPercent,
|
|
onChange: props.onChange,
|
|
})
|
|
);
|
|
}
|
|
|
|
function ColorSchemeDropDown(props) {
|
|
function onColorSchemeChange(value) {
|
|
props.onChange(value);
|
|
}
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Color Scheme" },
|
|
m$1(DropDown, {
|
|
selected: props.selected,
|
|
options: props.values,
|
|
onChange: onColorSchemeChange,
|
|
})
|
|
);
|
|
}
|
|
|
|
function FontPicker(props) {
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Font name" },
|
|
m$1(Select$1, {
|
|
class: {
|
|
"font-picker": true,
|
|
"font-picker--disabled": !props.theme.useFont,
|
|
},
|
|
value: props.theme.fontFamily,
|
|
onChange: props.onChange,
|
|
options: props.fonts.reduce((map, font) => {
|
|
map[font] = m$1("div", { style: { "font-family": font } }, font);
|
|
return map;
|
|
}, {}),
|
|
})
|
|
);
|
|
}
|
|
|
|
function Grayscale(props) {
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: getLocalMessage("grayscale") },
|
|
m$1(Slider, {
|
|
value: props.value,
|
|
min: 0,
|
|
max: 100,
|
|
step: 1,
|
|
formatValue: formatPercent,
|
|
onChange: props.onChange,
|
|
})
|
|
);
|
|
}
|
|
|
|
function ImmediateModify(props) {
|
|
const options = [
|
|
{ id: true, content: "Yes" },
|
|
{ id: false, content: "No" },
|
|
];
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Immediate modify" },
|
|
m$1(DropDown, {
|
|
options: options,
|
|
onChange: props.onChange,
|
|
selected: props.value,
|
|
})
|
|
);
|
|
}
|
|
|
|
var ThemeEngines = {
|
|
cssFilter: "cssFilter",
|
|
svgFilter: "svgFilter",
|
|
staticTheme: "staticTheme",
|
|
dynamicTheme: "dynamicTheme",
|
|
};
|
|
|
|
function Mode(props) {
|
|
function openCSSEditor() {
|
|
chrome.windows.create({
|
|
type: "panel",
|
|
url: isFirefox
|
|
? "../stylesheet-editor/index.html"
|
|
: "ui/stylesheet-editor/index.html",
|
|
width: 600,
|
|
height: 600,
|
|
});
|
|
}
|
|
const modes = [
|
|
{
|
|
id: ThemeEngines.dynamicTheme,
|
|
content: getLocalMessage("engine_dynamic"),
|
|
},
|
|
{ id: ThemeEngines.cssFilter, content: getLocalMessage("engine_filter") },
|
|
{
|
|
id: ThemeEngines.svgFilter,
|
|
content: getLocalMessage("engine_filter_plus"),
|
|
},
|
|
{
|
|
id: ThemeEngines.staticTheme,
|
|
content: getLocalMessage("engine_static"),
|
|
},
|
|
];
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Mode" },
|
|
m$1(
|
|
"div",
|
|
{ class: "mode-control-container" },
|
|
m$1(DropDown, {
|
|
selected: modes.find((m) => m.id === props.mode).id,
|
|
options: modes,
|
|
onChange: props.onChange,
|
|
}),
|
|
m$1("span", {
|
|
class: {
|
|
"static-edit-button": true,
|
|
"static-edit-button--hidden":
|
|
props.mode !== ThemeEngines.staticTheme,
|
|
},
|
|
onclick: openCSSEditor,
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
const DEFAULT_COLORS = {
|
|
darkScheme: {
|
|
background: "#181a1b",
|
|
text: "#e8e6e3",
|
|
},
|
|
lightScheme: {
|
|
background: "#dcdad7",
|
|
text: "#181a1b",
|
|
},
|
|
};
|
|
const DEFAULT_THEME = {
|
|
mode: 1,
|
|
brightness: 100,
|
|
contrast: 100,
|
|
grayscale: 0,
|
|
sepia: 0,
|
|
useFont: false,
|
|
fontFamily: isMacOS
|
|
? "Helvetica Neue"
|
|
: isWindows
|
|
? "Segoe UI"
|
|
: "Open Sans",
|
|
textStroke: 0,
|
|
engine: ThemeEngines.dynamicTheme,
|
|
stylesheet: "",
|
|
darkSchemeBackgroundColor: DEFAULT_COLORS.darkScheme.background,
|
|
darkSchemeTextColor: DEFAULT_COLORS.darkScheme.text,
|
|
lightSchemeBackgroundColor: DEFAULT_COLORS.lightScheme.background,
|
|
lightSchemeTextColor: DEFAULT_COLORS.lightScheme.text,
|
|
scrollbarColor: isMacOS ? "" : "auto",
|
|
selectionColor: "auto",
|
|
styleSystemControls: !isCSSColorSchemePropSupported,
|
|
lightColorScheme: "Default",
|
|
darkColorScheme: "Default",
|
|
immediateModify: false,
|
|
};
|
|
const DEFAULT_SETTINGS = {
|
|
enabled: true,
|
|
fetchNews: true,
|
|
theme: DEFAULT_THEME,
|
|
presets: [],
|
|
customThemes: [],
|
|
siteList: [],
|
|
siteListEnabled: [],
|
|
applyToListedOnly: false,
|
|
changeBrowserTheme: false,
|
|
syncSettings: true,
|
|
syncSitesFixes: false,
|
|
automation: "",
|
|
automationBehaviour: "OnOff",
|
|
time: {
|
|
activation: "18:00",
|
|
deactivation: "9:00",
|
|
},
|
|
location: {
|
|
latitude: null,
|
|
longitude: null,
|
|
},
|
|
previewNewDesign: false,
|
|
enableForPDF: true,
|
|
enableForProtectedPages: false,
|
|
enableContextMenus: false,
|
|
detectDarkTheme: false,
|
|
};
|
|
|
|
function ResetButtonGroup$1(props) {
|
|
function reset() {
|
|
props.actions.setTheme(DEFAULT_SETTINGS.theme);
|
|
}
|
|
return m$1(
|
|
ControlGroup$1,
|
|
null,
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(ResetButton, { onClick: reset }, "Reset to defaults")
|
|
),
|
|
m$1(
|
|
ControlGroup$1.Description,
|
|
null,
|
|
"Restore current theme values to defaults"
|
|
)
|
|
);
|
|
}
|
|
|
|
function Scheme(props) {
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Scheme" },
|
|
m$1(DropDown, {
|
|
selected: props.isDark,
|
|
options: [
|
|
{ id: true, content: getLocalMessage("dark") },
|
|
{ id: false, content: getLocalMessage("light") },
|
|
],
|
|
onChange: props.onChange,
|
|
})
|
|
);
|
|
}
|
|
|
|
function ScrollbarEditor(props) {
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Scrollbar" },
|
|
m$1(ColorDropDown, {
|
|
value: props.value,
|
|
colorSuggestion: "#959799",
|
|
onChange: props.onChange,
|
|
onReset: props.onReset,
|
|
hasAutoOption: true,
|
|
hasDefaultOption: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
function SelectionColorEditor(props) {
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Selection" },
|
|
m$1(ColorDropDown, {
|
|
value: props.value,
|
|
colorSuggestion: "#005ccc",
|
|
onChange: props.onChange,
|
|
onReset: props.onReset,
|
|
hasAutoOption: true,
|
|
hasDefaultOption: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
function Sepia(props) {
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: getLocalMessage("sepia") },
|
|
m$1(Slider, {
|
|
value: props.value,
|
|
min: 0,
|
|
max: 100,
|
|
step: 1,
|
|
formatValue: formatPercent,
|
|
onChange: props.onChange,
|
|
})
|
|
);
|
|
}
|
|
|
|
function StyleSystemControls(props) {
|
|
const options = [
|
|
{ id: true, content: "Yes" },
|
|
{ id: false, content: "No" },
|
|
];
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Style system controls" },
|
|
m$1(DropDown, {
|
|
options: options,
|
|
onChange: props.onChange,
|
|
selected: props.value,
|
|
})
|
|
);
|
|
}
|
|
|
|
function TextColorEditor(props) {
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Text" },
|
|
m$1(ColorPicker$1, {
|
|
color: props.value,
|
|
onChange: props.onChange,
|
|
canReset: props.canReset,
|
|
onReset: props.onReset,
|
|
})
|
|
);
|
|
}
|
|
|
|
function TextStroke(props) {
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Text stroke" },
|
|
m$1(Slider, {
|
|
value: props.value,
|
|
min: 0,
|
|
max: 1,
|
|
step: 0.1,
|
|
formatValue: String,
|
|
onChange: props.onChange,
|
|
})
|
|
);
|
|
}
|
|
|
|
function UseFont(props) {
|
|
const options = [
|
|
{ id: true, content: "Yes" },
|
|
{ id: false, content: "No" },
|
|
];
|
|
return m$1(
|
|
ThemeControl,
|
|
{ label: "Change font" },
|
|
m$1(DropDown, {
|
|
options: options,
|
|
onChange: props.onChange,
|
|
selected: props.value,
|
|
})
|
|
);
|
|
}
|
|
|
|
function hexify(number) {
|
|
return (number < 16 ? "0" : "") + number.toString(16);
|
|
}
|
|
function generateUID() {
|
|
if ("randomUUID" in crypto) {
|
|
const uuid = crypto.randomUUID();
|
|
return (
|
|
uuid.substring(0, 8) +
|
|
uuid.substring(9, 13) +
|
|
uuid.substring(14, 18) +
|
|
uuid.substring(19, 23) +
|
|
uuid.substring(24)
|
|
);
|
|
}
|
|
return Array.from(crypto.getRandomValues(new Uint8Array(16)))
|
|
.map((x) => hexify(x))
|
|
.join("");
|
|
}
|
|
|
|
function PresetItem(props) {
|
|
const context = getComponentContext();
|
|
const store = context.store;
|
|
function onRemoveClick(e) {
|
|
e.stopPropagation();
|
|
store.isConfirmationVisible = true;
|
|
context.refresh();
|
|
}
|
|
function onConfirmRemoveClick() {
|
|
const filtered = props.data.settings.presets.filter(
|
|
(p) => p.id !== props.preset.id
|
|
);
|
|
props.actions.changeSettings({ presets: filtered });
|
|
}
|
|
function onCancelRemoveClick() {
|
|
store.isConfirmationVisible = false;
|
|
context.refresh();
|
|
}
|
|
const confirmation = store.isConfirmationVisible
|
|
? m$1(MessageBox, {
|
|
caption: `Are you sure you want to remove ${props.preset.name}?`,
|
|
onOK: onConfirmRemoveClick,
|
|
onCancel: onCancelRemoveClick,
|
|
})
|
|
: null;
|
|
return m$1(
|
|
"span",
|
|
{ class: "theme-preset-picker__preset" },
|
|
m$1(
|
|
"span",
|
|
{ class: "theme-preset-picker__preset__name" },
|
|
props.preset.name
|
|
),
|
|
m$1("span", {
|
|
class: "theme-preset-picker__preset__remove-button",
|
|
onclick: onRemoveClick,
|
|
}),
|
|
confirmation
|
|
);
|
|
}
|
|
const MAX_ALLOWED_PRESETS = 3;
|
|
function PresetPicker(props) {
|
|
const tab = props.data.activeTab;
|
|
const host = getURLHostOrProtocol(tab.url);
|
|
const preset = props.data.settings.presets.find(({ urls }) =>
|
|
isURLInList(tab.url, urls)
|
|
);
|
|
const custom = props.data.settings.customThemes.find(({ url }) =>
|
|
isURLInList(tab.url, url)
|
|
);
|
|
const selectedPresetId = custom ? "custom" : preset ? preset.id : "default";
|
|
const defaultOption = { id: "default", content: "Theme for all websites" };
|
|
const addNewPresetOption =
|
|
props.data.settings.presets.length < MAX_ALLOWED_PRESETS
|
|
? { id: "add-preset", content: "\uff0b Create new theme" }
|
|
: null;
|
|
const userPresetsOptions = props.data.settings.presets.map((preset) => {
|
|
if (preset.id === selectedPresetId) {
|
|
return { id: preset.id, content: preset.name };
|
|
}
|
|
return {
|
|
id: preset.id,
|
|
content: m$1(PresetItem, { ...props, preset: preset }),
|
|
};
|
|
});
|
|
const customSitePresetOption = {
|
|
id: "custom",
|
|
content: `${
|
|
selectedPresetId === "custom" ? "\u2605" : "\u2606"
|
|
} Theme for ${host}`,
|
|
};
|
|
const dropdownOptions = [
|
|
defaultOption,
|
|
...userPresetsOptions,
|
|
addNewPresetOption,
|
|
customSitePresetOption,
|
|
].filter(Boolean);
|
|
function onPresetChange(id) {
|
|
const filteredCustomThemes = props.data.settings.customThemes.filter(
|
|
({ url }) => !isURLInList(tab.url, url)
|
|
);
|
|
const filteredPresets = props.data.settings.presets.map((preset) => {
|
|
return {
|
|
...preset,
|
|
urls: preset.urls.filter(
|
|
(template) => !isURLMatched(tab.url, template)
|
|
),
|
|
};
|
|
});
|
|
if (id === "default") {
|
|
props.actions.changeSettings({
|
|
customThemes: filteredCustomThemes,
|
|
presets: filteredPresets,
|
|
});
|
|
} else if (id === "custom") {
|
|
const extended = filteredCustomThemes.concat({
|
|
url: [host],
|
|
theme: { ...props.data.settings.theme },
|
|
});
|
|
props.actions.changeSettings({
|
|
customThemes: extended,
|
|
presets: filteredPresets,
|
|
});
|
|
} else if (id === "add-preset") {
|
|
let newPresetName;
|
|
for (let i = 0; i <= props.data.settings.presets.length; i++) {
|
|
newPresetName = `Theme ${i + 1}`;
|
|
if (
|
|
props.data.settings.presets.every((p) => p.name !== newPresetName)
|
|
) {
|
|
break;
|
|
}
|
|
}
|
|
const extended = filteredPresets.concat({
|
|
id: `preset-${generateUID()}`,
|
|
name: newPresetName,
|
|
urls: [host],
|
|
theme: { ...props.data.settings.theme },
|
|
});
|
|
props.actions.changeSettings({
|
|
customThemes: filteredCustomThemes,
|
|
presets: extended,
|
|
});
|
|
} else {
|
|
const chosenPresetId = id;
|
|
const extended = filteredPresets.map((preset) => {
|
|
if (preset.id === chosenPresetId) {
|
|
return {
|
|
...preset,
|
|
urls: preset.urls.concat(host),
|
|
};
|
|
}
|
|
return preset;
|
|
});
|
|
props.actions.changeSettings({
|
|
customThemes: filteredCustomThemes,
|
|
presets: extended,
|
|
});
|
|
}
|
|
}
|
|
return m$1(DropDown, {
|
|
class: "theme-preset-picker",
|
|
selected: selectedPresetId,
|
|
options: dropdownOptions,
|
|
onChange: onPresetChange,
|
|
});
|
|
}
|
|
|
|
function getCurrentThemePreset(props) {
|
|
const custom = props.data.settings.customThemes.find(({ url }) =>
|
|
isURLInList(props.data.activeTab.url, url)
|
|
);
|
|
const preset = custom
|
|
? null
|
|
: props.data.settings.presets.find(({ urls }) =>
|
|
isURLInList(props.data.activeTab.url, urls)
|
|
);
|
|
let theme = custom
|
|
? custom.theme
|
|
: preset
|
|
? preset.theme
|
|
: props.data.settings.theme;
|
|
if (props.data.forcedScheme) {
|
|
const mode = props.data.forcedScheme === "dark" ? 1 : 0;
|
|
theme = { ...theme, mode };
|
|
}
|
|
function setTheme(config) {
|
|
if (custom) {
|
|
custom.theme = { ...custom.theme, ...config };
|
|
props.actions.changeSettings({
|
|
customThemes: props.data.settings.customThemes,
|
|
});
|
|
} else if (preset) {
|
|
preset.theme = { ...preset.theme, ...config };
|
|
props.actions.changeSettings({
|
|
presets: props.data.settings.presets,
|
|
});
|
|
} else {
|
|
props.actions.setTheme(config);
|
|
}
|
|
if (
|
|
config.mode != null &&
|
|
props.data.settings.automation &&
|
|
props.data.settings.automationBehaviour === "Scheme"
|
|
) {
|
|
props.actions.changeSettings({
|
|
automation: "",
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
theme,
|
|
change: setTheme,
|
|
};
|
|
}
|
|
|
|
function ThemeControls(props) {
|
|
const { theme, onChange } = props;
|
|
return m$1(
|
|
"section",
|
|
{ class: "m-section m-theme-controls" },
|
|
m$1(Brightness, {
|
|
value: theme.brightness,
|
|
onChange: (v) => onChange({ brightness: v }),
|
|
}),
|
|
m$1(Contrast, {
|
|
value: theme.contrast,
|
|
onChange: (v) => onChange({ contrast: v }),
|
|
}),
|
|
m$1(Scheme, {
|
|
isDark: theme.mode === 1,
|
|
onChange: (isDark) => onChange({ mode: isDark ? 1 : 0 }),
|
|
}),
|
|
m$1(Mode, {
|
|
mode: theme.engine,
|
|
onChange: (mode) => onChange({ engine: mode }),
|
|
})
|
|
);
|
|
}
|
|
function ThemeGroup(props) {
|
|
const preset = getCurrentThemePreset(props);
|
|
return m$1(
|
|
"div",
|
|
{ class: "theme-group" },
|
|
m$1(
|
|
"div",
|
|
{ class: "theme-group__presets-wrapper" },
|
|
m$1(PresetPicker, { ...props })
|
|
),
|
|
m$1(
|
|
"div",
|
|
{ class: "theme-group__controls-wrapper" },
|
|
m$1(ThemeControls, { theme: preset.theme, onChange: preset.change }),
|
|
m$1(
|
|
Button,
|
|
{ class: "theme-group__more-button", onclick: props.onThemeNavClick },
|
|
"See all options"
|
|
)
|
|
),
|
|
m$1("label", { class: "theme-group__description" }, "Configure theme")
|
|
);
|
|
}
|
|
|
|
function SwitchGroup(props) {
|
|
return m$1(
|
|
Array,
|
|
null,
|
|
m$1(AppSwitch, { ...props }),
|
|
m$1(SiteToggleGroup, { ...props })
|
|
);
|
|
}
|
|
function SettingsNavButton(props) {
|
|
return m$1(
|
|
ResetButton$1,
|
|
{ onClick: props.onClick },
|
|
m$1("span", { class: "settings-button-icon" }),
|
|
"Settings"
|
|
);
|
|
}
|
|
function MainPage(props) {
|
|
return m$1(
|
|
Array,
|
|
null,
|
|
m$1("section", { class: "m-section" }, m$1(SwitchGroup, { ...props })),
|
|
m$1("section", { class: "m-section" }, m$1(ThemeGroup, { ...props })),
|
|
m$1(
|
|
"section",
|
|
{ class: "m-section" },
|
|
m$1(SettingsNavButton, { onClick: props.onSettingsNavClick }),
|
|
m$1(HelpGroup, null)
|
|
)
|
|
);
|
|
}
|
|
|
|
function isFresh(n) {
|
|
try {
|
|
const now = Date.now();
|
|
const date = new Date(n.date).getTime();
|
|
return now - date < getDuration({ days: 30 });
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
function NewsLink(props) {}
|
|
function NewsSection(props) {
|
|
const context = getComponentContext();
|
|
const store = context.getStore({
|
|
expanded: false,
|
|
didNewsSlideIn: false,
|
|
});
|
|
const { expanded, didNewsSlideIn } = store;
|
|
const news = props.data.news;
|
|
const latest = news && news.length > 0 ? news[0] : null;
|
|
function markLatestAsRead() {
|
|
if (latest) {
|
|
props.actions.markNewsAsRead([latest.id]);
|
|
}
|
|
}
|
|
function toggleNews() {
|
|
if (expanded) {
|
|
markLatestAsRead();
|
|
}
|
|
store.expanded = !expanded;
|
|
store.didNewsSlideIn = didNewsSlideIn || !store.expanded;
|
|
context.refresh();
|
|
}
|
|
context.onRender(() => {
|
|
if (latest && !latest.read && !store.expanded && !store.didNewsSlideIn) {
|
|
setTimeout(toggleNews, 750);
|
|
}
|
|
});
|
|
return m$1(
|
|
"div",
|
|
{ class: { "news-section": true, "news-section--expanded": expanded } },
|
|
latest
|
|
? m$1(NewsLink, {
|
|
isSmall: true,
|
|
news: latest,
|
|
onClick: markLatestAsRead,
|
|
})
|
|
: null,
|
|
m$1(
|
|
"div",
|
|
{ class: "news-section__popover" },
|
|
m$1(
|
|
"div",
|
|
{ class: "news-section__popover__top" },
|
|
m$1("div", { class: "news-section__title" }, "What's New"),
|
|
m$1(
|
|
"span",
|
|
{
|
|
role: "button",
|
|
class: "news-section__close",
|
|
onclick: toggleNews,
|
|
},
|
|
"\u2715"
|
|
)
|
|
),
|
|
latest
|
|
? m$1(NewsLink, { news: latest, onClick: markLatestAsRead })
|
|
: null
|
|
)
|
|
);
|
|
}
|
|
|
|
function Page(props, content) {
|
|
return m$1(
|
|
"div",
|
|
{ class: { page: true, "page--active": props.active } },
|
|
m$1("div", { class: "page__content" }, content),
|
|
props.first
|
|
? null
|
|
: m$1(
|
|
Button,
|
|
{ class: "page__back-button", onclick: props.onBackButtonClick },
|
|
"Back"
|
|
)
|
|
);
|
|
}
|
|
function PageViewer(props, ...pages) {
|
|
return m$1(
|
|
"div",
|
|
{ class: "page-viewer" },
|
|
pages.map((pageSpec, i) => {
|
|
return {
|
|
...pageSpec,
|
|
props: {
|
|
...pageSpec.props,
|
|
active: props.activePage === pageSpec.props.id,
|
|
first: i === 0,
|
|
onBackButtonClick: props.onBackButtonClick,
|
|
},
|
|
};
|
|
})
|
|
);
|
|
}
|
|
|
|
function AutomationButton(props) {
|
|
const now = new Date();
|
|
return m$1(
|
|
ControlGroup$1,
|
|
null,
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(
|
|
ResetButton$1,
|
|
{ onClick: props.onClick },
|
|
m$1(
|
|
"span",
|
|
{ class: "automation-button-icon" },
|
|
m$1(WatchIcon, { hours: now.getHours(), minutes: now.getMinutes() })
|
|
),
|
|
"Automation"
|
|
)
|
|
),
|
|
m$1(ControlGroup$1.Description, null, "Configure when app is enabled")
|
|
);
|
|
}
|
|
|
|
function getExistingDevToolsObject() {
|
|
if (isMobile) {
|
|
return new Promise((resolve) => {
|
|
chrome.tabs.query({}, (t) => {
|
|
for (const tab of t) {
|
|
if (tab.url.endsWith("ui/devtools/index.html")) {
|
|
resolve(tab);
|
|
return;
|
|
}
|
|
}
|
|
resolve(null);
|
|
});
|
|
});
|
|
}
|
|
return new Promise((resolve) => {
|
|
chrome.windows.getAll(
|
|
{
|
|
populate: true,
|
|
windowTypes: ["popup"],
|
|
},
|
|
(w) => {
|
|
for (const window of w) {
|
|
if (window.tabs[0].url.endsWith("ui/devtools/index.html")) {
|
|
resolve(window);
|
|
return;
|
|
}
|
|
}
|
|
resolve(null);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
async function openDevTools$1() {
|
|
const devToolsObject = await getExistingDevToolsObject();
|
|
if (isMobile) {
|
|
if (devToolsObject) {
|
|
chrome.tabs.update(devToolsObject.id, { active: true });
|
|
window.close();
|
|
} else {
|
|
chrome.tabs.create({
|
|
url: "../devtools/index.html",
|
|
});
|
|
window.close();
|
|
}
|
|
} else if (devToolsObject) {
|
|
chrome.windows.update(devToolsObject.id, { focused: true });
|
|
} else {
|
|
chrome.windows.create({
|
|
type: "popup",
|
|
url: isFirefox ? "../devtools/index.html" : "ui/devtools/index.html",
|
|
width: 600,
|
|
height: 600,
|
|
});
|
|
}
|
|
}
|
|
function DevToolsGroup(props) {
|
|
const globalThemeEngine = props.data.settings.theme.engine;
|
|
const devtoolsData = props.data.devtools;
|
|
const hasCustomFixes =
|
|
(globalThemeEngine === ThemeEngines.dynamicTheme &&
|
|
devtoolsData.hasCustomDynamicFixes) ||
|
|
([ThemeEngines.cssFilter, ThemeEngines.svgFilter].includes(
|
|
globalThemeEngine
|
|
) &&
|
|
devtoolsData.hasCustomFilterFixes) ||
|
|
(globalThemeEngine === ThemeEngines.staticTheme &&
|
|
devtoolsData.hasCustomStaticFixes);
|
|
return m$1(
|
|
ControlGroup$1,
|
|
null,
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(
|
|
ResetButton$1,
|
|
{
|
|
onClick: openDevTools$1,
|
|
class: {
|
|
"dev-tools-button": true,
|
|
"dev-tools-button--has-custom-fixes": hasCustomFixes,
|
|
},
|
|
},
|
|
"\uD83D\uDEE0 ",
|
|
getLocalMessage("open_dev_tools")
|
|
)
|
|
),
|
|
m$1(ControlGroup$1.Description, null, "Make a fix for a website")
|
|
);
|
|
}
|
|
|
|
function ManageSettingsButton(props) {
|
|
return m$1(
|
|
ControlGroup$1,
|
|
null,
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(ResetButton$1, { onClick: props.onClick }, "Manage settings")
|
|
),
|
|
m$1(ControlGroup$1.Description, null, "Reset, export or import settings")
|
|
);
|
|
}
|
|
|
|
function SiteListButton(props) {
|
|
return m$1(
|
|
ControlGroup$1,
|
|
null,
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(
|
|
ResetButton$1,
|
|
{ onClick: props.onClick },
|
|
m$1("span", { class: "site-list-button-icon" }),
|
|
"Site list"
|
|
)
|
|
),
|
|
m$1(
|
|
ControlGroup$1.Description,
|
|
null,
|
|
"Enable or disable on listed websites"
|
|
)
|
|
);
|
|
}
|
|
|
|
function CheckButton(props) {
|
|
return m$1(
|
|
ControlGroup$1,
|
|
{ class: "check-button" },
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(
|
|
CheckBox,
|
|
{
|
|
class: "check-button__checkbox",
|
|
checked: props.checked,
|
|
onchange: (e) => props.onChange(e.target.checked),
|
|
},
|
|
props.label
|
|
)
|
|
),
|
|
m$1(
|
|
ControlGroup$1.Description,
|
|
{ class: "check-button__description" },
|
|
props.description
|
|
)
|
|
);
|
|
}
|
|
|
|
function EnabledByDefaultGroup(props) {
|
|
function onEnabledByDefaultChange(checked) {
|
|
props.actions.changeSettings({ applyToListedOnly: !checked });
|
|
}
|
|
return m$1(CheckButton, {
|
|
checked: !props.data.settings.applyToListedOnly,
|
|
label: "Enable by default",
|
|
description: props.data.settings.applyToListedOnly
|
|
? "Disabled on all websites by default"
|
|
: "Enabled on all websites by default",
|
|
onChange: onEnabledByDefaultChange,
|
|
});
|
|
}
|
|
|
|
function DetectDarkTheme(props) {
|
|
function onDetectDarkThemeChange(checked) {
|
|
props.actions.changeSettings({ detectDarkTheme: checked });
|
|
}
|
|
return m$1(CheckButton, {
|
|
checked: props.data.settings.detectDarkTheme,
|
|
label: "Detect dark theme",
|
|
description: props.data.settings.detectDarkTheme
|
|
? `Will not override website's dark theme`
|
|
: `Will override website's dark theme`,
|
|
onChange: onDetectDarkThemeChange,
|
|
});
|
|
}
|
|
|
|
function ChangeBrowserTheme(props) {
|
|
function onBrowserThemeChange(checked) {
|
|
props.actions.changeSettings({ changeBrowserTheme: checked });
|
|
}
|
|
return m$1(CheckButton, {
|
|
checked: props.data.settings.changeBrowserTheme,
|
|
label: "Change browser theme",
|
|
description: props.data.settings.changeBrowserTheme
|
|
? "Custom browser theme is active"
|
|
: "Default browser theme is active",
|
|
onChange: onBrowserThemeChange,
|
|
});
|
|
}
|
|
|
|
function ContextMenusGroup(props) {
|
|
function onContextMenusChange(checked) {
|
|
if (checked) {
|
|
if (isFirefox) {
|
|
props.actions.changeSettings({ enableContextMenus: true });
|
|
return;
|
|
}
|
|
chrome.permissions.request(
|
|
{ permissions: ["contextMenus"] },
|
|
(hasPermission) => {
|
|
if (hasPermission) {
|
|
props.actions.changeSettings({ enableContextMenus: true });
|
|
} else {
|
|
console.warn("User declined contextMenus permission prompt.");
|
|
}
|
|
}
|
|
);
|
|
} else {
|
|
props.actions.changeSettings({ enableContextMenus: false });
|
|
}
|
|
}
|
|
return isMobile
|
|
? null
|
|
: m$1(CheckButton, {
|
|
checked: props.data.settings.enableContextMenus,
|
|
label: "Use context menus",
|
|
description: props.data.settings.enableContextMenus
|
|
? "Context menu integration is enabled"
|
|
: "Context menu integration is disabled",
|
|
onChange: onContextMenusChange,
|
|
});
|
|
}
|
|
|
|
let appVersion;
|
|
function AppVersion() {
|
|
if (!appVersion) {
|
|
appVersion = chrome.runtime.getManifest().version;
|
|
}
|
|
return m$1(
|
|
"label",
|
|
{ class: "darkreader-version" },
|
|
"Version 5 Preview (",
|
|
appVersion,
|
|
")"
|
|
);
|
|
}
|
|
|
|
function SettingsPage(props) {
|
|
return m$1(
|
|
"section",
|
|
{ class: "m-section" },
|
|
m$1(EnabledByDefaultGroup, { ...props }),
|
|
isFirefox ? m$1(ChangeBrowserTheme, { ...props }) : null,
|
|
m$1(SiteListButton, { onClick: props.onSiteListNavClick }),
|
|
m$1(DevToolsGroup, { ...props }),
|
|
m$1(AutomationButton, { onClick: props.onAutomationNavClick }),
|
|
m$1(ContextMenusGroup, { ...props }),
|
|
m$1(ManageSettingsButton, { onClick: props.onManageSettingsClick }),
|
|
m$1(DetectDarkTheme, { ...props }),
|
|
m$1(AppVersion, null)
|
|
);
|
|
}
|
|
|
|
function SiteList(props) {
|
|
const context = getComponentContext();
|
|
const store = context.store;
|
|
if (!context.prev) {
|
|
store.indices = new WeakMap();
|
|
store.shouldFocusAtIndex = -1;
|
|
store.wasVisible = false;
|
|
}
|
|
context.onRender((node) => {
|
|
const isVisible = node.clientWidth > 0;
|
|
const { wasVisible } = store;
|
|
store.wasVisible = isVisible;
|
|
if (!wasVisible && isVisible) {
|
|
store.shouldFocusAtIndex = props.siteList.length;
|
|
context.refresh();
|
|
}
|
|
});
|
|
function onTextChange(e) {
|
|
const index = store.indices.get(e.target);
|
|
const values = props.siteList.slice();
|
|
const value = e.target.value.trim();
|
|
if (values.includes(value)) {
|
|
return;
|
|
}
|
|
if (!value) {
|
|
values.splice(index, 1);
|
|
store.shouldFocusAtIndex = index;
|
|
} else if (index === values.length) {
|
|
values.push(value);
|
|
store.shouldFocusAtIndex = index + 1;
|
|
} else {
|
|
values.splice(index, 1, value);
|
|
store.shouldFocusAtIndex = index + 1;
|
|
}
|
|
props.onChange(values);
|
|
}
|
|
function removeValue(event) {
|
|
const previousSibling = event.target.previousSibling;
|
|
const index = store.indices.get(previousSibling);
|
|
const filtered = props.siteList.slice();
|
|
filtered.splice(index, 1);
|
|
store.shouldFocusAtIndex = index;
|
|
props.onChange(filtered);
|
|
}
|
|
function createTextBox(text, index) {
|
|
const onRender = (node) => {
|
|
store.indices.set(node, index);
|
|
if (store.shouldFocusAtIndex === index) {
|
|
store.shouldFocusAtIndex = -1;
|
|
node.focus();
|
|
}
|
|
};
|
|
return m$1(
|
|
"div",
|
|
{ class: "site-list__item" },
|
|
m$1(TextBox, {
|
|
class: "site-list__textbox",
|
|
value: text,
|
|
onrender: onRender,
|
|
placeholder: "google.com/maps",
|
|
}),
|
|
text
|
|
? m$1("span", {
|
|
class: "site-list__item__remove",
|
|
role: "button",
|
|
onclick: removeValue,
|
|
})
|
|
: null
|
|
);
|
|
}
|
|
return m$1(
|
|
"div",
|
|
{ class: "site-list" },
|
|
m$1(VirtualScroll, {
|
|
root: m$1("div", {
|
|
class: "site-list__v-scroll-root",
|
|
onchange: onTextChange,
|
|
}),
|
|
items: props.siteList
|
|
.map((site, index) => createTextBox(site, index))
|
|
.concat(createTextBox("", props.siteList.length)),
|
|
scrollToIndex: store.shouldFocusAtIndex,
|
|
})
|
|
);
|
|
}
|
|
|
|
function SiteListPage(props) {
|
|
function onSiteListChange(sites) {
|
|
props.actions.changeSettings({ siteList: sites });
|
|
}
|
|
function onInvertPDFChange(checked) {
|
|
props.actions.changeSettings({ enableForPDF: checked });
|
|
}
|
|
function onEnableForProtectedPages(value) {
|
|
props.actions.changeSettings({ enableForProtectedPages: value });
|
|
}
|
|
const label = props.data.settings.applyToListedOnly
|
|
? "Enable on these websites"
|
|
: "Disable on these websites";
|
|
return m$1(
|
|
"div",
|
|
{ class: "site-list-page" },
|
|
m$1("label", { class: "site-list-page__label" }, label),
|
|
m$1(SiteList, {
|
|
siteList: props.data.settings.siteList,
|
|
onChange: onSiteListChange,
|
|
}),
|
|
m$1(
|
|
"label",
|
|
{ class: "site-list-page__description" },
|
|
"Enter website name and press Enter"
|
|
),
|
|
isFirefox
|
|
? null
|
|
: m$1(CheckButton, {
|
|
checked: props.data.settings.enableForPDF,
|
|
label: "Enable for PDF files",
|
|
description: props.data.settings.enableForPDF
|
|
? "Enabled for PDF documents"
|
|
: "Disabled for PDF documents",
|
|
onChange: onInvertPDFChange,
|
|
}),
|
|
m$1(CheckButton, {
|
|
checked: props.data.settings.enableForProtectedPages,
|
|
onChange: onEnableForProtectedPages,
|
|
label: "Enable on restricted pages",
|
|
description: props.data.settings.enableForProtectedPages
|
|
? "You should enable it in browser flags too"
|
|
: "Disabled for web store and other pages",
|
|
})
|
|
);
|
|
}
|
|
|
|
function CollapsiblePanel({}, ...groups) {
|
|
const context = getComponentContext();
|
|
const store = context.store;
|
|
if (store.activeGroup == null) {
|
|
store.activeGroup = groups[0].props.id;
|
|
}
|
|
return m$1(
|
|
"div",
|
|
{ class: "collapsible-panel" },
|
|
...groups.map((spec, i) => {
|
|
const activeIndex = groups.findIndex(
|
|
(g) => store.activeGroup === g.props.id
|
|
);
|
|
const collapsed = i !== activeIndex;
|
|
const collapseTop = i < activeIndex;
|
|
const collapseBottom = i > activeIndex;
|
|
const onExpand = () => {
|
|
store.activeGroup = spec.props.id;
|
|
context.refresh();
|
|
};
|
|
return {
|
|
...spec,
|
|
props: {
|
|
...spec.props,
|
|
collapsed,
|
|
collapseBottom,
|
|
collapseTop,
|
|
onExpand,
|
|
},
|
|
};
|
|
})
|
|
);
|
|
}
|
|
function Group(props, content) {
|
|
return m$1(
|
|
"div",
|
|
{
|
|
class: {
|
|
"collapsible-panel__group": true,
|
|
"collapsible-panel__group--collapsed": props.collapsed,
|
|
"collapsible-panel__group--collapse-top": props.collapseTop,
|
|
"collapsible-panel__group--collapse-bottom": props.collapseBottom,
|
|
},
|
|
},
|
|
m$1("div", { class: "collapsible-panel__group__content" }, content),
|
|
m$1(
|
|
"span",
|
|
{
|
|
role: "button",
|
|
class: "collapsible-panel__group__expand-button",
|
|
onclick: props.onExpand,
|
|
},
|
|
props.label
|
|
)
|
|
);
|
|
}
|
|
var Collapsible = Object.assign(CollapsiblePanel, { Group });
|
|
|
|
function MainGroup({ theme, change }) {
|
|
return m$1(
|
|
Array,
|
|
null,
|
|
m$1(Brightness, {
|
|
value: theme.brightness,
|
|
onChange: (v) => change({ brightness: v }),
|
|
}),
|
|
m$1(Contrast, {
|
|
value: theme.contrast,
|
|
onChange: (v) => change({ contrast: v }),
|
|
}),
|
|
m$1(Sepia, { value: theme.sepia, onChange: (v) => change({ sepia: v }) }),
|
|
m$1(Grayscale, {
|
|
value: theme.grayscale,
|
|
onChange: (v) => change({ grayscale: v }),
|
|
}),
|
|
m$1(Scheme, {
|
|
isDark: theme.mode === 1,
|
|
onChange: (isDark) => change({ mode: isDark ? 1 : 0 }),
|
|
}),
|
|
m$1(Mode, {
|
|
mode: theme.engine,
|
|
onChange: (mode) => change({ engine: mode }),
|
|
})
|
|
);
|
|
}
|
|
function ColorsGroup({ theme, change, colorSchemes }) {
|
|
const isDarkScheme = theme.mode === 1;
|
|
const csProp = isDarkScheme ? "darkColorScheme" : "lightColorScheme";
|
|
const bgProp = isDarkScheme
|
|
? "darkSchemeBackgroundColor"
|
|
: "lightSchemeBackgroundColor";
|
|
const fgProp = isDarkScheme
|
|
? "darkSchemeTextColor"
|
|
: "lightSchemeTextColor";
|
|
const defaultSchemeColors = isDarkScheme
|
|
? DEFAULT_COLORS.darkScheme
|
|
: DEFAULT_COLORS.lightScheme;
|
|
const defaultMatrixValues = {
|
|
brightness: DEFAULT_THEME.brightness,
|
|
contrast: DEFAULT_THEME.contrast,
|
|
sepia: DEFAULT_THEME.sepia,
|
|
grayscale: DEFAULT_THEME.grayscale,
|
|
};
|
|
const currentColorScheme = isDarkScheme
|
|
? theme.darkColorScheme
|
|
: theme.lightColorScheme;
|
|
const currentSchemeColors = isDarkScheme
|
|
? colorSchemes.dark
|
|
: colorSchemes.light;
|
|
const sortedColorSchemeValues = Object.keys(currentSchemeColors)
|
|
.sort((a, b) => a.localeCompare(b))
|
|
.map((value) => {
|
|
return { id: value, content: value };
|
|
});
|
|
function onColorSchemeChange(newColor) {
|
|
change({ [csProp]: newColor });
|
|
change({ [bgProp]: currentSchemeColors[newColor].backgroundColor });
|
|
change({ [fgProp]: currentSchemeColors[newColor].textColor });
|
|
}
|
|
return m$1(
|
|
Array,
|
|
null,
|
|
m$1(BackgroundColorEditor, {
|
|
value:
|
|
theme[bgProp] === "auto"
|
|
? defaultSchemeColors.background
|
|
: theme[bgProp],
|
|
onChange: (v) =>
|
|
change({ [bgProp]: v, ...defaultMatrixValues, [csProp]: "Default" }),
|
|
canReset: theme[bgProp] !== defaultSchemeColors.background,
|
|
onReset: () =>
|
|
change({
|
|
[bgProp]: DEFAULT_SETTINGS.theme[bgProp],
|
|
[csProp]: "Default",
|
|
}),
|
|
}),
|
|
m$1(TextColorEditor, {
|
|
value:
|
|
theme[fgProp] === "auto" ? defaultSchemeColors.text : theme[fgProp],
|
|
onChange: (v) =>
|
|
change({ [fgProp]: v, ...defaultMatrixValues, [csProp]: "Default" }),
|
|
canReset: theme[fgProp] !== defaultSchemeColors.text,
|
|
onReset: () =>
|
|
change({
|
|
[fgProp]: DEFAULT_SETTINGS.theme[fgProp],
|
|
[csProp]: "Default",
|
|
}),
|
|
}),
|
|
m$1(ScrollbarEditor, {
|
|
value: theme.scrollbarColor,
|
|
onChange: (v) => change({ scrollbarColor: v }),
|
|
onReset: () =>
|
|
change({ scrollbarColor: DEFAULT_SETTINGS.theme.scrollbarColor }),
|
|
}),
|
|
m$1(SelectionColorEditor, {
|
|
value: theme.selectionColor,
|
|
onChange: (v) => change({ selectionColor: v }),
|
|
onReset: () =>
|
|
change({ selectionColor: DEFAULT_SETTINGS.theme.selectionColor }),
|
|
}),
|
|
m$1(ColorSchemeDropDown, {
|
|
selected: currentColorScheme,
|
|
values: sortedColorSchemeValues,
|
|
onChange: (v) => onColorSchemeChange(v),
|
|
})
|
|
);
|
|
}
|
|
function FontGroup({ theme, fonts, change }) {
|
|
return m$1(
|
|
Array,
|
|
null,
|
|
m$1(UseFont, {
|
|
value: theme.useFont,
|
|
onChange: (useFont) => change({ useFont }),
|
|
}),
|
|
m$1(FontPicker, {
|
|
theme: theme,
|
|
fonts: fonts,
|
|
onChange: (fontFamily) => change({ fontFamily }),
|
|
}),
|
|
m$1(TextStroke, {
|
|
value: theme.textStroke,
|
|
onChange: (textStroke) => change({ textStroke }),
|
|
}),
|
|
m$1(StyleSystemControls, {
|
|
value: theme.styleSystemControls,
|
|
onChange: (styleSystemControls) => change({ styleSystemControls }),
|
|
}),
|
|
m$1(ImmediateModify, {
|
|
value: theme.immediateModify,
|
|
onChange: (immediateModify) => change({ immediateModify }),
|
|
})
|
|
);
|
|
}
|
|
function ThemePage(props) {
|
|
const { theme, change } = getCurrentThemePreset(props);
|
|
return m$1(
|
|
"section",
|
|
{ class: "m-section theme-page" },
|
|
m$1(PresetPicker, { ...props }),
|
|
m$1(
|
|
Collapsible,
|
|
null,
|
|
m$1(
|
|
Collapsible.Group,
|
|
{ id: "main", label: "Brightness, contrast, mode" },
|
|
m$1(MainGroup, { theme: theme, change: change })
|
|
),
|
|
m$1(
|
|
Collapsible.Group,
|
|
{ id: "colors", label: "Colors" },
|
|
m$1(ColorsGroup, {
|
|
theme: theme,
|
|
change: change,
|
|
colorSchemes: props.data.colorScheme,
|
|
})
|
|
),
|
|
m$1(
|
|
Collapsible.Group,
|
|
{ id: "font", label: "Font & more" },
|
|
m$1(FontGroup, { theme: theme, fonts: props.fonts, change: change })
|
|
)
|
|
),
|
|
m$1(ResetButtonGroup$1, { ...props })
|
|
);
|
|
}
|
|
|
|
function ResetButtonGroup(props) {
|
|
const context = getComponentContext();
|
|
function showDialog() {
|
|
context.store.isDialogVisible = true;
|
|
context.refresh();
|
|
}
|
|
function hideDialog() {
|
|
context.store.isDialogVisible = false;
|
|
context.refresh();
|
|
}
|
|
function reset() {
|
|
context.store.isDialogVisible = false;
|
|
props.actions.changeSettings(DEFAULT_SETTINGS);
|
|
}
|
|
const dialog = context.store.isDialogVisible
|
|
? m$1(MessageBox, {
|
|
caption:
|
|
"Are you sure you want to remove all your settings? You cannot restore them later",
|
|
onOK: reset,
|
|
onCancel: hideDialog,
|
|
})
|
|
: null;
|
|
return m$1(
|
|
ControlGroup$1,
|
|
null,
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(ResetButton, { onClick: showDialog }, "Reset settings", dialog)
|
|
),
|
|
m$1(ControlGroup$1.Description, null, "Restore settings to defaults")
|
|
);
|
|
}
|
|
|
|
function ImportButton(props) {
|
|
const context = getComponentContext();
|
|
function getValidatedObject(source, compare) {
|
|
const result = {};
|
|
if (
|
|
source == null ||
|
|
typeof source !== "object" ||
|
|
Array.isArray(source)
|
|
) {
|
|
return null;
|
|
}
|
|
Object.keys(source).forEach((key) => {
|
|
const value = source[key];
|
|
if (value == null || compare[key] == null) {
|
|
return;
|
|
}
|
|
const array1 = Array.isArray(value);
|
|
const array2 = Array.isArray(compare[key]);
|
|
if (array1 || array2) {
|
|
if (array1 && array2) {
|
|
result[key] = value;
|
|
}
|
|
} else if (
|
|
typeof value === "object" &&
|
|
typeof compare[key] === "object"
|
|
) {
|
|
result[key] = getValidatedObject(value, compare[key]);
|
|
} else if (typeof value === typeof compare[key]) {
|
|
result[key] = value;
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
function showDialog() {
|
|
context.store.isDialogVisible = true;
|
|
context.refresh();
|
|
}
|
|
function hideDialog() {
|
|
context.store.isDialogVisible = false;
|
|
context.refresh();
|
|
}
|
|
const dialog =
|
|
context && context.store.isDialogVisible
|
|
? m$1(MessageBox, {
|
|
caption: "The given file has incorrect JSON.",
|
|
onOK: hideDialog,
|
|
onCancel: hideDialog,
|
|
})
|
|
: null;
|
|
function importSettings() {
|
|
openFile({ extensions: ["json"] }, (result) => {
|
|
try {
|
|
const content = JSON.parse(result);
|
|
const result2 = getValidatedObject(content, DEFAULT_SETTINGS);
|
|
props.actions.changeSettings({ ...result2 });
|
|
} catch (err) {
|
|
console.error(err);
|
|
showDialog();
|
|
}
|
|
});
|
|
}
|
|
return m$1(
|
|
ControlGroup$1,
|
|
null,
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(
|
|
Button,
|
|
{ onclick: importSettings, class: "settings-button" },
|
|
"Import Settings",
|
|
dialog
|
|
)
|
|
),
|
|
m$1(ControlGroup$1.Description, null, "Open settings from a JSON file")
|
|
);
|
|
}
|
|
|
|
function ExportButton(props) {
|
|
function exportSettings() {
|
|
saveFile("Settings.json", JSON.stringify(props.data.settings, null, 4));
|
|
}
|
|
return m$1(
|
|
ControlGroup$1,
|
|
null,
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(
|
|
Button,
|
|
{ onclick: exportSettings, class: "settings-button" },
|
|
"Export Settings"
|
|
)
|
|
),
|
|
m$1(ControlGroup$1.Description, null, "Save settings to a JSON file")
|
|
);
|
|
}
|
|
|
|
function SyncSettings(props) {
|
|
function onSyncSettingsChange(checked) {
|
|
props.actions.changeSettings({ syncSettings: checked });
|
|
}
|
|
return m$1(CheckButton, {
|
|
checked: props.data.settings.syncSettings,
|
|
label: "Enable settings sync",
|
|
description: props.data.settings.syncSettings
|
|
? "Synchronized across devices"
|
|
: "Not synchronized across devices",
|
|
onChange: onSyncSettingsChange,
|
|
});
|
|
}
|
|
|
|
function ExportTheme() {
|
|
const listener = ({ type, data }, sender) => {
|
|
if (type === MessageType.CS_EXPORT_CSS_RESPONSE) {
|
|
const url = getURLHostOrProtocol(sender.tab.url).replace(
|
|
/[^a-z0-1\-]/g,
|
|
"-"
|
|
);
|
|
saveFile(`DarkMode-${url}.css`, data);
|
|
chrome.runtime.onMessage.removeListener(listener);
|
|
}
|
|
};
|
|
function exportCSS() {
|
|
chrome.runtime.onMessage.addListener(listener);
|
|
chrome.runtime.sendMessage({ type: MessageType.UI_REQUEST_EXPORT_CSS });
|
|
}
|
|
return m$1(
|
|
ControlGroup$1,
|
|
null,
|
|
m$1(
|
|
ControlGroup$1.Control,
|
|
null,
|
|
m$1(
|
|
Button,
|
|
{ onclick: exportCSS, class: "settings-button" },
|
|
"Export Dynamic Theme"
|
|
)
|
|
),
|
|
m$1(ControlGroup$1.Description, null, "Save generated CSS to a file")
|
|
);
|
|
}
|
|
|
|
function SyncConfigButton(props) {
|
|
function syncConfig(syncSitesFixes) {
|
|
props.actions.changeSettings({ syncSitesFixes });
|
|
props.actions.loadConfig({ local: !syncSitesFixes });
|
|
}
|
|
return m$1(CheckButton, {
|
|
checked: props.data.settings.syncSitesFixes,
|
|
label: "Synchronize sites fixes",
|
|
description: "Load the latest sites fixes from a remote server",
|
|
onChange: syncConfig,
|
|
});
|
|
}
|
|
|
|
function FetchNews(props) {
|
|
function onFetchNewsChange(checked) {
|
|
props.actions.changeSettings({ fetchNews: checked });
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function ManageSettingsPage(props) {
|
|
const custom = props.data.settings.customThemes.find(({ url }) =>
|
|
isURLInList(props.data.activeTab.url, url)
|
|
);
|
|
const engine = custom
|
|
? custom.theme.engine
|
|
: props.data.settings.theme.engine;
|
|
return m$1(
|
|
"section",
|
|
{ class: "m-section" },
|
|
m$1(SyncSettings, { ...props }),
|
|
m$1(SyncConfigButton, { ...props }),
|
|
m$1(FetchNews, { ...props }),
|
|
m$1(ImportButton, { ...props }),
|
|
m$1(ExportButton, { ...props }),
|
|
engine === ThemeEngines.dynamicTheme ? m$1(ExportTheme, null) : null,
|
|
m$1(ResetButtonGroup, { ...props })
|
|
);
|
|
}
|
|
|
|
function Logo() {
|
|
// return m$1("a", { class: "m-logo", href: "", target: "_blank", rel: "noopener noreferrer" }, "");
|
|
}
|
|
function Pages(props) {
|
|
const context = getComponentContext();
|
|
const store = context.store;
|
|
if (store.activePage == null) {
|
|
store.activePage = "main";
|
|
}
|
|
function onThemeNavClick() {
|
|
store.activePage = "theme";
|
|
context.refresh();
|
|
}
|
|
function onSettingsNavClick() {
|
|
store.activePage = "settings";
|
|
context.refresh();
|
|
}
|
|
function onAutomationNavClick() {
|
|
store.activePage = "automation";
|
|
context.refresh();
|
|
}
|
|
function onManageSettingsClick() {
|
|
store.activePage = "manage-settings";
|
|
context.refresh();
|
|
}
|
|
function onSiteListNavClick() {
|
|
store.activePage = "site-list";
|
|
context.refresh();
|
|
}
|
|
function onBackClick() {
|
|
const activePage = store.activePage;
|
|
const settingsPageSubpages = [
|
|
"automation",
|
|
"manage-settings",
|
|
"site-list",
|
|
];
|
|
if (settingsPageSubpages.includes(activePage)) {
|
|
store.activePage = "settings";
|
|
} else {
|
|
store.activePage = "main";
|
|
}
|
|
context.refresh();
|
|
}
|
|
return m$1(
|
|
PageViewer,
|
|
{ activePage: store.activePage, onBackButtonClick: onBackClick },
|
|
m$1(
|
|
Page,
|
|
{ id: "main" },
|
|
m$1(MainPage, {
|
|
...props,
|
|
onThemeNavClick: onThemeNavClick,
|
|
onSettingsNavClick: onSettingsNavClick,
|
|
})
|
|
),
|
|
m$1(Page, { id: "theme" }, m$1(ThemePage, { ...props })),
|
|
m$1(
|
|
Page,
|
|
{ id: "settings" },
|
|
m$1(SettingsPage, {
|
|
...props,
|
|
onAutomationNavClick: onAutomationNavClick,
|
|
onManageSettingsClick: onManageSettingsClick,
|
|
onSiteListNavClick: onSiteListNavClick,
|
|
})
|
|
),
|
|
m$1(Page, { id: "site-list" }, m$1(SiteListPage, { ...props })),
|
|
m$1(Page, { id: "automation" }, m$1(AutomationPage, { ...props })),
|
|
m$1(
|
|
Page,
|
|
{ id: "manage-settings" },
|
|
m$1(ManageSettingsPage, { ...props })
|
|
)
|
|
);
|
|
}
|
|
function DonateGroup() {
|
|
return m$1(
|
|
"div",
|
|
{ class: "m-donate-group" },
|
|
m$1(
|
|
"a",
|
|
{
|
|
class: "m-donate-button",
|
|
href: DONATE_URL,
|
|
target: "_blank",
|
|
rel: "noopener noreferrer",
|
|
},
|
|
m$1("span", { class: "m-donate-button__icon" }),
|
|
m$1(
|
|
"span",
|
|
{ class: "m-donate-button__text" },
|
|
getLocalMessage("donate")
|
|
)
|
|
),
|
|
m$1(
|
|
"label",
|
|
{ class: "m-donate-description" },
|
|
"This project is sponsored by you"
|
|
)
|
|
);
|
|
}
|
|
function Body$2(props) {
|
|
const context = getComponentContext();
|
|
context.onCreate(() => {
|
|
if (isMobile) {
|
|
window.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
}
|
|
});
|
|
context.onRemove(() => {
|
|
document.documentElement.classList.remove("preview");
|
|
});
|
|
return m$1(
|
|
"body",
|
|
null,
|
|
m$1("section", { class: "m-section" }, m$1(Logo, null)),
|
|
m$1(
|
|
"section",
|
|
{ class: "m-section pages-section" },
|
|
m$1(Pages, { ...props })
|
|
),
|
|
m$1("section", { class: "m-section" }, m$1(DonateGroup, null)),
|
|
m$1(NewsSection, { ...props }),
|
|
m$1(Overlay$1, null)
|
|
);
|
|
}
|
|
|
|
const engineNames = [
|
|
[ThemeEngines.cssFilter, getLocalMessage("engine_filter")],
|
|
[ThemeEngines.svgFilter, getLocalMessage("engine_filter_plus")],
|
|
[ThemeEngines.staticTheme, getLocalMessage("engine_static")],
|
|
[ThemeEngines.dynamicTheme, getLocalMessage("engine_dynamic")],
|
|
];
|
|
function openCSSEditor() {
|
|
chrome.windows.create({
|
|
type: "panel",
|
|
url: isFirefox
|
|
? "../stylesheet-editor/index.html"
|
|
: "ui/stylesheet-editor/index.html",
|
|
width: 600,
|
|
height: 600,
|
|
});
|
|
}
|
|
function EngineSwitch({ engine, onChange }) {
|
|
return m$1(
|
|
"div",
|
|
{ class: "engine-switch" },
|
|
m$1(MultiSwitch, {
|
|
value: engineNames.find(([code]) => code === engine)[1],
|
|
options: engineNames.map(([, name]) => name),
|
|
onChange: (value) =>
|
|
onChange(engineNames.find(([, name]) => name === value)[0]),
|
|
}),
|
|
m$1("span", {
|
|
class: {
|
|
"engine-switch__css-edit-button": true,
|
|
"engine-switch__css-edit-button_active":
|
|
engine === ThemeEngines.staticTheme,
|
|
},
|
|
onclick: openCSSEditor,
|
|
}),
|
|
m$1(
|
|
"label",
|
|
{ class: "engine-switch__description" },
|
|
getLocalMessage("theme_generation_mode")
|
|
)
|
|
);
|
|
}
|
|
|
|
function FontSettings({ config, fonts, onChange }) {
|
|
return m$1(
|
|
"section",
|
|
{ class: "font-settings" },
|
|
m$1(
|
|
"div",
|
|
{ class: "font-settings__font-select-container" },
|
|
m$1(
|
|
"div",
|
|
{ class: "font-settings__font-select-container__line" },
|
|
m$1(CheckBox, {
|
|
checked: config.useFont,
|
|
onchange: (e) => onChange({ useFont: e.target.checked }),
|
|
}),
|
|
m$1(Select$1, {
|
|
value: config.fontFamily,
|
|
onChange: (value) => onChange({ fontFamily: value }),
|
|
options: fonts.reduce((map, font) => {
|
|
map[font] = m$1("div", { style: { "font-family": font } }, font);
|
|
return map;
|
|
}, {}),
|
|
})
|
|
),
|
|
m$1(
|
|
"label",
|
|
{ class: "font-settings__font-select-container__label" },
|
|
getLocalMessage("select_font")
|
|
)
|
|
),
|
|
m$1(UpDown, {
|
|
value: config.textStroke,
|
|
min: 0,
|
|
max: 1,
|
|
step: 0.1,
|
|
default: 0,
|
|
name: getLocalMessage("text_stroke"),
|
|
onChange: (value) => onChange({ textStroke: value }),
|
|
})
|
|
);
|
|
}
|
|
|
|
function compileMarkdown(markdown) {
|
|
return markdown
|
|
.split("**")
|
|
.map((text, i) => (i % 2 ? m$1("strong", null, text) : text));
|
|
}
|
|
|
|
function MoreSettings({ data, actions, fonts }) {
|
|
const tab = data.activeTab;
|
|
const custom = data.settings.customThemes.find(({ url }) =>
|
|
isURLInList(tab.url, url)
|
|
);
|
|
const filterConfig = custom ? custom.theme : data.settings.theme;
|
|
function setConfig(config) {
|
|
if (custom) {
|
|
custom.theme = { ...custom.theme, ...config };
|
|
actions.changeSettings({ customThemes: data.settings.customThemes });
|
|
} else {
|
|
actions.setTheme(config);
|
|
}
|
|
}
|
|
return m$1(
|
|
"section",
|
|
{ class: "more-settings" },
|
|
m$1(
|
|
"div",
|
|
{ class: "more-settings__section" },
|
|
m$1(FontSettings, {
|
|
config: filterConfig,
|
|
fonts: fonts,
|
|
onChange: setConfig,
|
|
})
|
|
),
|
|
m$1(
|
|
"div",
|
|
{ class: "more-settings__section" },
|
|
isFirefox
|
|
? null
|
|
: m$1(
|
|
"p",
|
|
{ class: "more-settings__description" },
|
|
compileMarkdown(getLocalMessage("try_experimental_theme_engines"))
|
|
),
|
|
m$1(EngineSwitch, {
|
|
engine: filterConfig.engine,
|
|
onChange: (engine) => setConfig({ engine }),
|
|
})
|
|
),
|
|
m$1(
|
|
"div",
|
|
{ class: "more-settings__section" },
|
|
m$1(CustomSettingsToggle, { data: data, actions: actions }),
|
|
tab.isProtected
|
|
? m$1(
|
|
"p",
|
|
{
|
|
class:
|
|
"more-settings__description more-settings__description--warning",
|
|
},
|
|
getLocalMessage("page_protected").replace(/\n/g, " ")
|
|
)
|
|
: tab.isInDarkList
|
|
? m$1(
|
|
"p",
|
|
{
|
|
class:
|
|
"more-settings__description more-settings__description--warning",
|
|
},
|
|
getLocalMessage("page_in_dark_list").replace(/\n/g, " ")
|
|
)
|
|
: m$1(
|
|
"p",
|
|
{ class: "more-settings__description" },
|
|
getLocalMessage("only_for_description")
|
|
)
|
|
),
|
|
isFirefox
|
|
? m$1(
|
|
"div",
|
|
{ class: "more-settings__section" },
|
|
m$1(Toggle, {
|
|
checked: data.settings.changeBrowserTheme,
|
|
labelOn: getLocalMessage("custom_browser_theme_on"),
|
|
labelOff: getLocalMessage("custom_browser_theme_off"),
|
|
onChange: (checked) =>
|
|
actions.changeSettings({ changeBrowserTheme: checked }),
|
|
}),
|
|
m$1(
|
|
"p",
|
|
{ class: "more-settings__description" },
|
|
getLocalMessage("change_browser_theme")
|
|
)
|
|
)
|
|
: null
|
|
);
|
|
}
|
|
|
|
function NewsGroup({ news, expanded, onNewsOpen, onClose }) {
|
|
return 0;
|
|
}
|
|
function NewsButton({ active, count, onClick }) {
|
|
return m$1(
|
|
Button,
|
|
{
|
|
class: { "news-button": true, "news-button--active": active },
|
|
href: "#news",
|
|
"data-count": count > 0 && !active ? count : null,
|
|
onclick: (e) => {
|
|
e.currentTarget.blur();
|
|
onClick();
|
|
},
|
|
},
|
|
getLocalMessage("news")
|
|
);
|
|
}
|
|
|
|
function SiteListSettings({ data, actions, isFocused }) {
|
|
function isSiteUrlValid(value) {
|
|
return /^([^\.\s]+?\.?)+$/.test(value);
|
|
}
|
|
return m$1(
|
|
"section",
|
|
{ class: "site-list-settings" },
|
|
m$1(Toggle, {
|
|
class: "site-list-settings__toggle",
|
|
checked: data.settings.applyToListedOnly,
|
|
labelOn: getLocalMessage("invert_listed_only"),
|
|
labelOff: getLocalMessage("not_invert_listed"),
|
|
onChange: (value) =>
|
|
actions.changeSettings({ applyToListedOnly: value }),
|
|
}),
|
|
m$1(TextList, {
|
|
class: "site-list-settings__text-list",
|
|
placeholder: "google.com/maps",
|
|
values: data.settings.siteList,
|
|
isFocused: isFocused,
|
|
onChange: (values) => {
|
|
if (values.every(isSiteUrlValid)) {
|
|
actions.changeSettings({ siteList: values });
|
|
}
|
|
},
|
|
}),
|
|
m$1(ShortcutLink, {
|
|
class: "site-list-settings__shortcut",
|
|
commandName: "addSite",
|
|
shortcuts: data.shortcuts,
|
|
textTemplate: (hotkey) =>
|
|
hotkey
|
|
? `${getLocalMessage("add_site_to_list")}: ${hotkey}`
|
|
: getLocalMessage("setup_add_site_hotkey"),
|
|
onSetShortcut: (shortcut) => actions.setShortcut("addSite", shortcut),
|
|
})
|
|
);
|
|
}
|
|
|
|
function openDevTools() {
|
|
chrome.windows.create({
|
|
type: "panel",
|
|
url: isFirefox ? "../devtools/index.html" : "ui/devtools/index.html",
|
|
width: 600,
|
|
height: 600,
|
|
});
|
|
}
|
|
function Body(props) {
|
|
const context = getComponentContext();
|
|
const { state, setState } = useState({
|
|
activeTab: "Filter",
|
|
newsOpen: false,
|
|
didNewsSlideIn: false,
|
|
moreToggleSettingsOpen: false,
|
|
});
|
|
if (!props.data.isReady) {
|
|
return m$1("body", null, m$1(Loader$1, { complete: false }));
|
|
}
|
|
if (isMobile || props.data.settings.previewNewDesign) {
|
|
return m$1(Body$2, { ...props, fonts: props.fonts });
|
|
}
|
|
const unreadNews = props.data.news.filter(({ read }) => !read);
|
|
const latestNews = props.data.news.length > 0 ? props.data.news[0] : null;
|
|
const isFirstNewsUnread = latestNews && !latestNews.read;
|
|
context.onRender(() => {
|
|
if (
|
|
props.data.settings.fetchNews &&
|
|
isFirstNewsUnread &&
|
|
!state.newsOpen &&
|
|
!state.didNewsSlideIn
|
|
) {
|
|
setTimeout(toggleNews, 750);
|
|
}
|
|
});
|
|
function toggleNews() {
|
|
if (state.newsOpen && unreadNews.length > 0) {
|
|
props.actions.markNewsAsRead(unreadNews.map(({ id }) => id));
|
|
}
|
|
setState({
|
|
newsOpen: !state.newsOpen,
|
|
didNewsSlideIn: state.didNewsSlideIn || !state.newsOpen,
|
|
});
|
|
}
|
|
function onNewsOpen(...news) {
|
|
const unread = news.filter(({ read }) => !read);
|
|
if (unread.length > 0) {
|
|
props.actions.markNewsAsRead(unread.map(({ id }) => id));
|
|
}
|
|
}
|
|
let displayedNewsCount = unreadNews.length;
|
|
if (unreadNews.length > 0) {
|
|
const latest = new Date(unreadNews[0].date);
|
|
const today = new Date();
|
|
const newsWereLongTimeAgo =
|
|
latest.getTime() < today.getTime() - getDuration({ days: 14 });
|
|
if (newsWereLongTimeAgo) {
|
|
displayedNewsCount = 0;
|
|
}
|
|
}
|
|
const globalThemeEngine = props.data.settings.theme.engine;
|
|
const devtoolsData = props.data.devtools;
|
|
const hasCustomFixes =
|
|
(globalThemeEngine === ThemeEngines.dynamicTheme &&
|
|
devtoolsData.hasCustomDynamicFixes) ||
|
|
([ThemeEngines.cssFilter, ThemeEngines.svgFilter].includes(
|
|
globalThemeEngine
|
|
) &&
|
|
devtoolsData.hasCustomFilterFixes) ||
|
|
(globalThemeEngine === ThemeEngines.staticTheme &&
|
|
devtoolsData.hasCustomStaticFixes);
|
|
function toggleMoreToggleSettings() {
|
|
setState({ moreToggleSettingsOpen: !state.moreToggleSettingsOpen });
|
|
}
|
|
return m$1(
|
|
"body",
|
|
{ class: { "ext-disabled": !props.data.isEnabled } },
|
|
m$1(Loader$1, { complete: true }),
|
|
m$1(Header, {
|
|
data: props.data,
|
|
actions: props.actions,
|
|
onMoreToggleSettingsClick: toggleMoreToggleSettings,
|
|
}),
|
|
m$1(TabPanel, {
|
|
activeTab: state.activeTab,
|
|
onSwitchTab: (tab) => setState({ activeTab: tab }),
|
|
tabs: isThunderbird
|
|
? {
|
|
Filter: m$1(FilterSettings, {
|
|
data: props.data,
|
|
actions: props.actions,
|
|
}),
|
|
More: m$1(MoreSettings, {
|
|
data: props.data,
|
|
actions: props.actions,
|
|
fonts: props.fonts,
|
|
}),
|
|
}
|
|
: {
|
|
Filter: m$1(FilterSettings, {
|
|
data: props.data,
|
|
actions: props.actions,
|
|
}),
|
|
"Site list": m$1(SiteListSettings, {
|
|
data: props.data,
|
|
actions: props.actions,
|
|
isFocused: state.activeTab === "Site list",
|
|
}),
|
|
More: m$1(MoreSettings, {
|
|
data: props.data,
|
|
actions: props.actions,
|
|
fonts: props.fonts,
|
|
}),
|
|
},
|
|
tabLabels: {
|
|
Filter: getLocalMessage("filter"),
|
|
"Site list": getLocalMessage("site_list"),
|
|
More: getLocalMessage("more"),
|
|
},
|
|
})
|
|
);
|
|
}
|
|
var Body$1 = compose(Body, withState, withForms);
|
|
|
|
function popupHasBuiltInBorders() {
|
|
return Boolean(
|
|
chromiumVersion &&
|
|
!isVivaldi &&
|
|
!isYaBrowser &&
|
|
!isOpera &&
|
|
isWindows &&
|
|
compareChromeVersions(chromiumVersion, "62.0.3167.0") < 0
|
|
);
|
|
}
|
|
function popupHasBuiltInHorizontalBorders() {
|
|
return Boolean(
|
|
chromiumVersion &&
|
|
!isVivaldi &&
|
|
!isYaBrowser &&
|
|
!isEdge &&
|
|
!isOpera &&
|
|
((isWindows &&
|
|
compareChromeVersions(chromiumVersion, "62.0.3167.0") >= 0 &&
|
|
compareChromeVersions(chromiumVersion, "74.0.0.0") < 0) ||
|
|
(isMacOS &&
|
|
compareChromeVersions(chromiumVersion, "67.0.3373.0") >= 0 &&
|
|
compareChromeVersions(chromiumVersion, "73.0.3661.0") < 0))
|
|
);
|
|
}
|
|
function fixNotClosingPopupOnNavigation() {
|
|
document.addEventListener("click", (e) => {
|
|
if (e.defaultPrevented || e.button === 2) {
|
|
return;
|
|
}
|
|
let target = e.target;
|
|
while (target && !(target instanceof HTMLAnchorElement)) {
|
|
target = target.parentElement;
|
|
}
|
|
if (target && target.hasAttribute("href")) {
|
|
chrome.tabs.create({ url: target.getAttribute("href") });
|
|
e.preventDefault();
|
|
if (!isThunderbird) {
|
|
window.close();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderBody(data, fonts, actions) {
|
|
if (data.settings.previewNewDesign) {
|
|
if (!document.documentElement.classList.contains("preview")) {
|
|
document.documentElement.classList.add("preview");
|
|
}
|
|
} else if (document.documentElement.classList.contains("preview")) {
|
|
document.documentElement.classList.remove("preview");
|
|
}
|
|
if (data.news && data.news.length > 0) {
|
|
const latest = data.news[0];
|
|
if (latest && !latest.displayed) {
|
|
actions.markNewsAsDisplayed([latest.id]);
|
|
}
|
|
}
|
|
sync(
|
|
document.body,
|
|
m$1(Body$1, { data: data, actions: actions, fonts: fonts })
|
|
);
|
|
}
|
|
async function start() {
|
|
const connector = new Connector();
|
|
window.addEventListener("unload", () => connector.disconnect());
|
|
const [data, fonts] = await Promise.all([
|
|
connector.getData(),
|
|
getFontList(),
|
|
]);
|
|
renderBody(data, fonts, connector);
|
|
connector.subscribeToChanges((data) => renderBody(data, fonts, connector));
|
|
}
|
|
addEventListener("load", start);
|
|
document.documentElement.classList.toggle("mobile", isMobile);
|
|
document.documentElement.classList.toggle("firefox", isFirefox);
|
|
document.documentElement.classList.toggle(
|
|
"built-in-borders",
|
|
popupHasBuiltInBorders()
|
|
);
|
|
document.documentElement.classList.toggle(
|
|
"built-in-horizontal-borders",
|
|
popupHasBuiltInHorizontalBorders()
|
|
);
|
|
if (isFirefox) {
|
|
fixNotClosingPopupOnNavigation();
|
|
}
|
|
})();
|