formats and more of dollar

This commit is contained in:
Lalle 2023-04-18 02:45:38 +02:00
parent e23dd2366a
commit f79199366e
No known key found for this signature in database
GPG Key ID: A6583D207A8F6B0D

View File

@ -18,19 +18,19 @@ const $ = (selector: string, root: HTMLElement = doc) => root.querySelector(sele
$.id = id => d.getElementById(id); $.id = id => d.getElementById(id);
$.ready = function(fc) { $.ready = function (fc) {
if (d.readyState !== 'loading') { if (d.readyState !== 'loading') {
$.queueTask(fc); $.queueTask(fc);
return; return;
} }
var cb = function() { var cb = function () {
$.off(d, 'DOMContentLoaded', cb); $.off(d, 'DOMContentLoaded', cb);
return fc(); return fc();
}; };
return $.on(d, 'DOMContentLoaded', cb); return $.on(d, 'DOMContentLoaded', cb);
}; };
$.formData = function(form) { $.formData = function (form) {
if (form instanceof HTMLFormElement) { if (form instanceof HTMLFormElement) {
return new FormData(form); return new FormData(form);
} }
@ -48,7 +48,7 @@ $.formData = function(form) {
return fd; return fd;
}; };
$.extend = function(object, properties) { $.extend = function (object, properties) {
for (var key in properties) { for (var key in properties) {
var val = properties[key]; var val = properties[key];
object[key] = val; object[key] = val;
@ -57,11 +57,11 @@ $.extend = function(object, properties) {
$.hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); $.hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
$.getOwn = function(obj, key) { $.getOwn = function (obj, key) {
if (Object.prototype.hasOwnProperty.call(obj, key)) { return obj[key]; } else { return undefined; } if (Object.prototype.hasOwnProperty.call(obj, key)) { return obj[key]; } else { return undefined; }
}; };
$.ajax = (function() { $.ajax = (function () {
let pageXHR; let pageXHR;
if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) { if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) {
pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest); pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest);
@ -69,7 +69,7 @@ $.ajax = (function() {
pageXHR = XMLHttpRequest; pageXHR = XMLHttpRequest;
} }
const r = (function (url, options={}) { const r = (function (url, options = {}) {
if (options.responseType == null) { options.responseType = 'json'; } if (options.responseType == null) { options.responseType = 'json'; }
if (!options.type) { options.type = (options.form && 'post') || 'get'; } if (!options.type) { options.type = (options.form && 'post') || 'get'; }
// XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310 // XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310
@ -80,7 +80,7 @@ $.ajax = (function() {
return $.ajaxPage(url, options); return $.ajaxPage(url, options);
} }
} }
const {onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers} = options; const { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options;
const r = new pageXHR(); const r = new pageXHR();
try { try {
r.open(type, url, true); r.open(type, url, true);
@ -89,15 +89,15 @@ $.ajax = (function() {
var value = object[key]; var value = object[key];
r.setRequestHeader(key, value); r.setRequestHeader(key, value);
} }
$.extend(r, {onloadend, timeout, responseType, withCredentials}); $.extend(r, { onloadend, timeout, responseType, withCredentials });
$.extend(r.upload, {onprogress}); $.extend(r.upload, { onprogress });
// connection error or content blocker // connection error or content blocker
$.on(r, 'error', function() { if (!r.status) { return c.warn(`4chan X failed to load: ${url}`); } }); $.on(r, 'error', function () { if (!r.status) { return c.warn(`4chan X failed to load: ${url}`); } });
if (platform === 'crx') { if (platform === 'crx') {
// https://bugs.chromium.org/p/chromium/issues/detail?id=920638 // https://bugs.chromium.org/p/chromium/issues/detail?id=920638
$.on(r, 'load', () => { $.on(r, 'load', () => {
if (!Conf['Work around CORB Bug'] && r.readyState === 4 && r.status === 200 && r.statusText === '' && r.response === null) { if (!Conf['Work around CORB Bug'] && r.readyState === 4 && r.status === 200 && r.statusText === '' && r.response === null) {
$.set('Work around CORB Bug', (Conf['Work around CORB Bug'] = Date.now())); $.set('Work around CORB Bug', (Conf['Work around CORB Bug'] = Date.now()), cb => cb());
} }
}); });
} }
@ -106,8 +106,7 @@ $.ajax = (function() {
// XXX Some content blockers in Firefox (e.g. Adblock Plus and NoScript) throw an exception instead of simulating a connection error. // XXX Some content blockers in Firefox (e.g. Adblock Plus and NoScript) throw an exception instead of simulating a connection error.
if (err.result !== 0x805e0006) { throw err; } if (err.result !== 0x805e0006) { throw err; }
r.onloadend = onloadend; r.onloadend = onloadend;
$.queueTask($.event, 'error', null, r); r.onerror();
$.queueTask($.event, 'loadend', null, r);
} }
return r; return r;
}); });
@ -115,17 +114,16 @@ $.ajax = (function() {
if (platform === 'userscript') { if (platform === 'userscript') {
return r; return r;
} else { } else {
// # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638 let requestID: number
let requestID = 0;
const requests = dict(); const requests = dict();
$.ajaxPageInit = function() { $.ajaxPageInit = function () {
$.global(function() { $.global(function () {
window.FCX.requests = Object.create(null); window.FCX.requests = Object.create(null);
document.addEventListener('4chanXAjax', function(e) { document.addEventListener('4chanXAjax', function (e) {
let fd, r; let fd: FormData, r: XMLHttpRequest;
const {url, timeout, responseType, withCredentials, type, onprogress, form, headers, id} = e.detail; const { url, timeout, responseType, withCredentials, type, onprogress, form, headers, id } = e.detail;
window.FCX.requests[id] = (r = new XMLHttpRequest()); window.FCX.requests[id] = (r = new XMLHttpRequest());
r.open(type, url, true); r.open(type, url, true);
const object = headers || {}; const object = headers || {};
@ -137,51 +135,42 @@ $.ajax = (function() {
r.timeout = timeout; r.timeout = timeout;
r.withCredentials = withCredentials; r.withCredentials = withCredentials;
if (onprogress) { if (onprogress) {
r.upload.onprogress = function(e) { r.upload.onprogress = function (e: ProgressEvent) {
const {loaded, total} = e; const { loaded, total } = e;
const detail = {loaded, total, id}; const detail = { loaded, total, id };
return document.dispatchEvent(new CustomEvent('4chanXAjaxProgress', {bubbles: true, detail})); return document.dispatchEvent(new CustomEvent('4chanXAjaxProgress', { bubbles: true, detail }));
}; };
} }
r.onloadend = function() { r.onloadend = function () {
delete window.FCX.requests[id]; delete window.FCX.requests[id];
const {status, statusText, response} = this; const { status, statusText, response } = this;
const responseHeaderString = this.getAllResponseHeaders(); const responseHeaderString = this.getAllResponseHeaders();
const detail = {status, statusText, response, responseHeaderString, id}; const detail = { status, statusText, response, responseHeaderString, id };
return document.dispatchEvent(new CustomEvent('4chanXAjaxLoadend', {bubbles: true, detail})); return document.dispatchEvent(new CustomEvent('4chanXAjaxLoadend', { bubbles: true, detail }));
}; };
// connection error or content blocker // connection error or content blocker
r.onerror = function() { r.onerror = function () {
if (!r.status) { return console.warn(`4chan X failed to load: ${url}`); } if (!r.status) { return console.warn(`4chan X failed to load: ${url}`); }
}; };
if (form) { if (form) {
fd = new FormData(); fd = new FormData();
for (var entry of form) { for (var [key, value] of form.entries()) {
fd.append(entry[0], entry[1]); fd.append(key, value);
} }
} else {
fd = null;
} }
return r.send(fd); return r.send(fd);
} });
, false); }, 'ajaxPageInit');
};
return document.addEventListener('4chanXAjaxAbort', function(e) { $.on(d, '4chanXAjaxProgress', function (e: CustomEvent) {
let r;
if (!(r = window.FCX.requests[e.detail.id])) { return; }
return r.abort();
}
, false);
});
$.on(d, '4chanXAjaxProgress', function(e) {
let req; let req;
if (!(req = requests[e.detail.id])) { return; } if (!(req = requests[e.detail.id])) { return; }
return req.upload.onprogress.call(req.upload, e.detail); return req.upload.onprogress.call(req.upload, e.detail);
}); });
return $.on(d, '4chanXAjaxLoadend', function(e) { return $.on(d, '4chanXAjaxLoadend', function (e: CustomEvent) {
let req; let req: XMLHttpRequest;
if (!(req = requests[e.detail.id])) { return; } if (!(req = requests[e.detail.id])) { return; }
delete requests[e.detail.id]; delete requests[e.detail.id];
if (e.detail.status) { if (e.detail.status) {
@ -192,22 +181,8 @@ $.ajax = (function() {
req.response = new DOMParser().parseFromString(e.detail.response, 'text/html'); req.response = new DOMParser().parseFromString(e.detail.response, 'text/html');
} }
} }
return req.onloadend(); return req.onloadend.call(req);
}); });
};
return $.ajaxPage = function(url, options={}) {
let req;
let {onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers} = options;
const id = requestID++;
requests[id] = (req = new CrossOrigin.Request());
$.extend(req, {responseType, onloadend});
req.upload = {onprogress};
req.abort = () => $.event('4chanXAjaxAbort', {id});
if (form) { form = Array.from(form.entries()); }
$.event('4chanXAjax', {url, timeout, responseType, withCredentials, type, onprogress: !!onprogress, form, headers, id});
return req;
};
} }
})(); })();
@ -215,9 +190,9 @@ $.ajax = (function() {
// With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses. // With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
// This saves a lot of bandwidth and CPU time for both the users and the servers. // This saves a lot of bandwidth and CPU time for both the users and the servers.
$.lastModified = dict(); $.lastModified = dict();
$.whenModified = function(url, bucket, cb, options={}) { $.whenModified = function (url, bucket, cb, options = {}) {
let t; let t;
const {timeout, ajax} = options; const { timeout, ajax } = options;
const params = []; const params = [];
// XXX https://bugs.chromium.org/p/chromium/issues/detail?id=643659 // XXX https://bugs.chromium.org/p/chromium/issues/detail?id=643659
if ($.engine === 'blink') { params.push(`s=${bucket}`); } if ($.engine === 'blink') { params.push(`s=${bucket}`); }
@ -239,33 +214,33 @@ $.whenModified = function(url, bucket, cb, options={}) {
return r; return r;
}; };
(function() { (function () {
const reqs = dict(); const reqs = dict();
$.cache = function(url, cb, options={}) { $.cache = function (url, cb, options = {}) {
let req; let req;
const {ajax} = options; const { ajax } = options;
if (req = reqs[url]) { if (req = reqs[url]) {
if (req.callbacks) { if (req.callbacks) {
req.callbacks.push(cb); req.callbacks.push(cb);
} else { } else {
$.queueTask(() => cb.call(req, {isCached: true})); $.queueTask(() => cb.call(req, { isCached: true }));
} }
return req; return req;
} }
const onloadend = function() { const onloadend = function () {
if (!this.status) { if (!this.status) {
delete reqs[url]; delete reqs[url];
} }
for (cb of this.callbacks) { for (cb of this.callbacks) {
(cb => $.queueTask(() => cb.call(this, {isCached: false})))(cb); (cb => $.queueTask(() => cb.call(this, { isCached: false })))(cb);
} }
return delete this.callbacks; return delete this.callbacks;
}; };
req = (ajax || $.ajax)(url, {onloadend}); req = (ajax || $.ajax)(url, { onloadend });
req.callbacks = [cb]; req.callbacks = [cb];
return reqs[url] = req; return reqs[url] = req;
}; };
return $.cleanCache = function(testf) { return $.cleanCache = function (testf) {
for (var url in reqs) { for (var url in reqs) {
if (testf(url)) { if (testf(url)) {
delete reqs[url]; delete reqs[url];
@ -289,7 +264,7 @@ $.cb = {
} }
}; };
$.asap = function(test, cb) { $.asap = function (test, cb) {
if (test()) { if (test()) {
return cb(); return cb();
} else { } else {
@ -297,32 +272,32 @@ $.asap = function(test, cb) {
} }
}; };
$.onExists = function(root, selector, cb) { $.onExists = function (root, selector, cb) {
let el; let el;
if (el = $(selector, root)) { if (el = $(selector, root)) {
return cb(el); return cb(el);
} }
var observer = new MutationObserver(function() { var observer = new MutationObserver(function () {
if (el = $(selector, root)) { if (el = $(selector, root)) {
observer.disconnect(); observer.disconnect();
return cb(el); return cb(el);
} }
}); });
return observer.observe(root, {childList: true, subtree: true}); return observer.observe(root, { childList: true, subtree: true });
}; };
$.addStyle = function(css, id, test='head') { $.addStyle = function (css, id, test = 'head') {
const style = $.el('style', const style = $.el('style',
{textContent: css}); { textContent: css });
if (id != null) { style.id = id; } if (id != null) { style.id = id; }
$.onExists(doc, test, () => $.add(d.head, style)); $.onExists(doc, test, () => $.add(d.head, style));
return style; return style;
}; };
$.addCSP = function(policy) { $.addCSP = function (policy) {
const meta = $.el('meta', { const meta = $.el('meta', {
httpEquiv: 'Content-Security-Policy', httpEquiv: 'Content-Security-Policy',
content: policy content: policy
} }
); );
if (d.head) { if (d.head) {
@ -335,23 +310,23 @@ $.addCSP = function(policy) {
} }
}; };
$.x = function(path, root) { $.x = function (path, root) {
if (!root) { root = d.body; } if (!root) { root = d.body; }
// XPathResult.ANY_UNORDERED_NODE_TYPE === 8 // XPathResult.ANY_UNORDERED_NODE_TYPE === 8
return d.evaluate(path, root, null, 8, null).singleNodeValue; return d.evaluate(path, root, null, 8, null).singleNodeValue;
}; };
$.X = function(path, root) { $.X = function (path, root) {
if (!root) { root = d.body; } if (!root) { root = d.body; }
// XPathResult.ORDERED_NODE_SNAPSHOT_TYPE === 7 // XPathResult.ORDERED_NODE_SNAPSHOT_TYPE === 7
return d.evaluate(path, root, null, 7, null); return d.evaluate(path, root, null, 7, null);
}; };
$.addClass = function(el, ...classNames) { $.addClass = function (el, ...classNames) {
for (var className of classNames) { el.classList.add(className); } for (var className of classNames) { el.classList.add(className); }
}; };
$.rmClass = function(el, ...classNames) { $.rmClass = function (el, ...classNames) {
for (var className of classNames) { el.classList.remove(className); } for (var className of classNames) { el.classList.remove(className); }
}; };
@ -362,13 +337,13 @@ $.hasClass = (el, className) => el.classList.contains(className);
$.rm = el => el?.remove(); $.rm = el => el?.remove();
$.rmAll = root => // https://gist.github.com/MayhemYDG/8646194 $.rmAll = root => // https://gist.github.com/MayhemYDG/8646194
root.textContent = null; root.textContent = null;
$.tn = s => d.createTextNode(s); $.tn = s => d.createTextNode(s);
$.frag = () => d.createDocumentFragment(); $.frag = () => d.createDocumentFragment();
$.nodes = function(nodes) { $.nodes = function (nodes) {
if (!(nodes instanceof Array)) { if (!(nodes instanceof Array)) {
return nodes; return nodes;
} }
@ -389,55 +364,55 @@ $.before = (root, el) => root.parentNode.insertBefore($.nodes(el), root);
$.replace = (root, el) => root.parentNode.replaceChild($.nodes(el), root); $.replace = (root, el) => root.parentNode.replaceChild($.nodes(el), root);
$.el = function(tag, properties, properties2) { $.el = function (tag, properties, properties2) {
const el = d.createElement(tag); const el = d.createElement(tag);
if (properties) { $.extend(el, properties); } if (properties) { $.extend(el, properties); }
if (properties2) { $.extend(el, properties2); } if (properties2) { $.extend(el, properties2); }
return el; return el;
}; };
$.on = function(el, events, handler) { $.on = function (el, events, handler) {
for (var event of events.split(' ')) { for (var event of events.split(' ')) {
el.addEventListener(event, handler, false); el.addEventListener(event, handler, false);
} }
}; };
$.off = function(el, events, handler) { $.off = function (el, events, handler) {
for (var event of events.split(' ')) { for (var event of events.split(' ')) {
el.removeEventListener(event, handler, false); el.removeEventListener(event, handler, false);
} }
}; };
$.one = function(el, events, handler) { $.one = function (el, events, handler) {
var cb = function(e) { var cb = function (e) {
$.off(el, events, cb); $.off(el, events, cb);
return handler.call(this, e); return handler.call(this, e);
}; };
return $.on(el, events, cb); return $.on(el, events, cb);
}; };
$.event = function(event, detail, root=d) { $.event = function (event, detail, root = d) {
if (!globalThis.chrome?.extension) { if (!globalThis.chrome?.extension) {
if ((detail != null) && (typeof cloneInto === 'function')) { if ((detail != null) && (typeof cloneInto === 'function')) {
detail = cloneInto(detail, d.defaultView); detail = cloneInto(detail, d.defaultView);
} }
} }
return root.dispatchEvent(new CustomEvent(event, {bubbles: true, cancelable: true, detail})); return root.dispatchEvent(new CustomEvent(event, { bubbles: true, cancelable: true, detail }));
}; };
if (platform === 'userscript') { if (platform === 'userscript') {
// XXX Make $.event work in Pale Moon with GM 3.x (no cloneInto function). // XXX Make $.event work in Pale Moon with GM 3.x (no cloneInto function).
(function() { (function () {
if (!/PaleMoon\//.test(navigator.userAgent) || (+GM_info?.version?.split('.')[0] < 2) || (typeof cloneInto !== 'undefined')) { return; } if (!/PaleMoon\//.test(navigator.userAgent) || (+GM_info?.version?.split('.')[0] < 2) || (typeof cloneInto !== 'undefined')) { return; }
try { try {
return new CustomEvent('x', {detail: {}}); return new CustomEvent('x', { detail: {} });
} catch (err) { } catch (err) {
const unsafeConstructors = { const unsafeConstructors = {
Object: unsafeWindow.Object, Object: unsafeWindow.Object,
Array: unsafeWindow.Array Array: unsafeWindow.Array
}; };
var clone = function(obj) { var clone = function (obj) {
let constructor; let constructor;
if ((obj != null) && (typeof obj === 'object') && (constructor = unsafeConstructors[obj.constructor.name])) { if ((obj != null) && (typeof obj === 'object') && (constructor = unsafeConstructors[obj.constructor.name])) {
const obj2 = new constructor(); const obj2 = new constructor();
@ -447,7 +422,7 @@ if (platform === 'userscript') {
return obj; return obj;
} }
}; };
return $.event = (event, detail, root=d) => root.dispatchEvent(new CustomEvent(event, {bubbles: true, cancelable: true, detail: clone(detail)})); return $.event = (event, detail, root = d) => root.dispatchEvent(new CustomEvent(event, { bubbles: true, cancelable: true, detail: clone(detail) }));
} }
})(); })();
} }
@ -458,25 +433,25 @@ if (!globalThis.chrome?.extension) {
$.open = $.open =
(GM?.openInTab != null) ? (GM?.openInTab != null) ?
GM.openInTab GM.openInTab
: (typeof GM_openInTab !== 'undefined' && GM_openInTab !== null) ? : (typeof GM_openInTab !== 'undefined' && GM_openInTab !== null) ?
GM_openInTab GM_openInTab
: :
url => window.open(url, '_blank'); url => window.open(url, '_blank');
} else { } else {
$.open = $.open =
url => window.open(url, '_blank'); url => window.open(url, '_blank');
} }
$.debounce = function(wait, fn) { $.debounce = function (wait, fn) {
let lastCall = 0; let lastCall = 0;
let timeout = null; let timeout = null;
let that = null; let that = null;
let args = null; let args = null;
const exec = function() { const exec = function () {
lastCall = Date.now(); lastCall = Date.now();
return fn.apply(that, args); return fn.apply(that, args);
}; };
return function() { return function () {
args = arguments; args = arguments;
that = this; that = this;
if (lastCall < (Date.now() - wait)) { if (lastCall < (Date.now() - wait)) {
@ -489,34 +464,17 @@ $.debounce = function(wait, fn) {
}; };
}; };
$.queueTask = (function() { $.queueTask = function (fn: VoidFunction) {
// inspired by https://www.w3.org/Bugs/Public/show_bug.cgi?id=15007 if (typeof queueMicrotask === 'function') {
const taskQueue = []; return queueMicrotask(fn);
const execTask = function() { } else {
const task = taskQueue.shift(); return setTimeout(fn, 0);
const func = task[0];
const args = Array.prototype.slice.call(task, 1);
return func.apply(func, args);
};
if (window.MessageChannel) {
const taskChannel = new MessageChannel();
taskChannel.port1.onmessage = execTask;
return function() {
taskQueue.push(arguments);
return taskChannel.port2.postMessage(null);
};
} else { // XXX Firefox
return function() {
taskQueue.push(arguments);
return setTimeout(execTask, 0);
};
} }
})(); };
$.global = function(fn, data) { $.global = function (fn, data) {
if (doc) { if (doc) {
const script = $.el('script', const script = $.el('script', { type: 'application/json' }, { textContent: JSON.stringify(data) });
{textContent: `(${fn}).call(document.currentScript.dataset);`});
if (data) { $.extend(script.dataset, data); } if (data) { $.extend(script.dataset, data); }
$.add((d.head || doc), script); $.add((d.head || doc), script);
$.rm(script); $.rm(script);
@ -525,12 +483,12 @@ $.global = function(fn, data) {
// XXX dwb // XXX dwb
try { try {
fn.call(data); fn.call(data);
} catch (error) {} } catch (error) { }
return data; return data;
} }
}; };
$.bytesToString = function(size) { $.bytesToString = function (size: number) {
let unit = 0; // Bytes let unit = 0; // Bytes
while (size >= 1024) { while (size >= 1024) {
size /= 1024; size /= 1024;
@ -542,40 +500,39 @@ $.bytesToString = function(size) {
// Keep the size as a float if the size is greater than 2^20 B. // Keep the size as a float if the size is greater than 2^20 B.
// Round to hundredth. // Round to hundredth.
Math.round(size * 100) / 100 Math.round(size * 100) / 100
: :
// Round to an integer otherwise. // Round to an integer otherwise.
Math.round(size); Math.round(size);
return `${size} ${['B', 'KB', 'MB', 'GB'][unit]}`; return `${size} ${['B', 'KB', 'MB', 'GB'][unit]}`;
}; };
$.minmax = (value, min, max) => value < min ? $.minmax = (value: number, min: number, max: number) => value < min ?
min min
: : value > max ?
value > max ?
max max
: :
value; value;
$.hasAudio = video => video.mozHasAudio || !!video.webkitAudioDecodedByteCount; $.hasAudio = video => video.mozHasAudio || !!video.webkitAudioDecodedByteCount;
$.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114); $.luma = rgb => (rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114);
$.unescape = function(text) { $.unescape = function (text) {
if (text == null) { return text; } if (text == null) { return text; }
return text.replace(/<[^>]*>/g, '').replace(/&(amp|#039|quot|lt|gt|#44);/g, c => ({'&amp;': '&', '&#039;': "'", '&quot;': '"', '&lt;': '<', '&gt;': '>', '&#44;': ','})[c]); return text.replace(/<[^>]*>/g, '').replace(/&(amp|#039|quot|lt|gt|#44);/g, c => ({ '&amp;': '&', '&#039;': "'", '&quot;': '"', '&lt;': '<', '&gt;': '>', '&#44;': ',' })[c]);
}; };
$.isImage = url => /\.(jpe?g|jfif|png|gif|bmp|webp|avif|jxl)$/i.test(url); $.isImage = url => /\.(jpe?g|jfif|png|gif|bmp|webp|avif|jxl)$/i.test(url);
$.isVideo = url => /\.(webm|mp4|ogv)$/i.test(url); $.isVideo = url => /\.(webm|mp4|ogv)$/i.test(url);
$.engine = (function() { $.engine = (function () {
if (/Edge\//.test(navigator.userAgent)) { return 'edge'; } if (/Edge\//.test(navigator.userAgent)) { return 'edge'; }
if (/Chrome\//.test(navigator.userAgent)) { return 'blink'; } if (/Chrome\//.test(navigator.userAgent)) { return 'blink'; }
if (/WebKit\//.test(navigator.userAgent)) { return 'webkit'; } if (/WebKit\//.test(navigator.userAgent)) { return 'webkit'; }
if (/Gecko\/|Goanna/.test(navigator.userAgent)) { return 'gecko'; } // Goanna = Pale Moon 26+ if (/Gecko\/|Goanna/.test(navigator.userAgent)) { return 'gecko'; } // Goanna = Pale Moon 26+
})(); })();
$.hasStorage = (function() { $.hasStorage = (function () {
try { try {
if (localStorage.getItem(g.NAMESPACE + 'hasStorage') === 'true') { return true; } if (localStorage.getItem(g.NAMESPACE + 'hasStorage') === 'true') { return true; }
localStorage.setItem(g.NAMESPACE + 'hasStorage', 'true'); localStorage.setItem(g.NAMESPACE + 'hasStorage', 'true');
@ -585,13 +542,13 @@ $.hasStorage = (function() {
} }
})(); })();
$.item = function(key, val) { $.item = function (key, val) {
const item = dict(); const item = dict();
item[key] = val; item[key] = val;
return item; return item;
}; };
$.oneItemSugar = fn => (function(key, val, cb) { $.oneItemSugar = fn => (function (key, val, cb) {
if (typeof key === 'string') { if (typeof key === 'string') {
return fn($.item(key, val), cb); return fn($.item(key, val), cb);
} else { } else {
@ -601,7 +558,7 @@ $.oneItemSugar = fn => (function(key, val, cb) {
$.syncing = dict(); $.syncing = dict();
$.securityCheck = function(data) { $.securityCheck = function (data) {
if (location.protocol !== 'https:') { if (location.protocol !== 'https:') {
return delete data['Redirect to HTTPS']; return delete data['Redirect to HTTPS'];
} }
@ -611,10 +568,10 @@ if (platform === 'crx') {
// https://developer.chrome.com/extensions/storage.html // https://developer.chrome.com/extensions/storage.html
$.oldValue = { $.oldValue = {
local: dict(), local: dict(),
sync: dict() sync: dict()
}; };
chrome.storage.onChanged.addListener(function(changes, area) { chrome.storage.onChanged.addListener(function (changes, area) {
for (var key in changes) { for (var key in changes) {
var oldValue = $.oldValue.local[key] ?? $.oldValue.sync[key]; var oldValue = $.oldValue.local[key] ?? $.oldValue.sync[key];
$.oldValue[area][key] = dict.clone(changes[key].newValue); $.oldValue[area][key] = dict.clone(changes[key].newValue);
@ -626,17 +583,17 @@ if (platform === 'crx') {
} }
}); });
$.sync = (key, cb) => $.syncing[key] = cb; $.sync = (key, cb) => $.syncing[key] = cb;
$.forceSync = function() { }; $.forceSync = function () { };
$.crxWorking = function() { $.crxWorking = function () {
try { try {
if (chrome.runtime.getManifest()) { if (chrome.runtime.getManifest()) {
return true; return true;
} }
} catch (error) {} } catch (error) { }
if (!$.crxWarningShown) { if (!$.crxWarningShown) {
const msg = $.el('div', const msg = $.el('div',
{innerHTML: '4chan X seems to have been updated. You will need to <a href="javascript:;">reload</a> the page.'}); { innerHTML: '4chan X seems to have been updated. You will need to <a href="javascript:;">reload</a> the page.' });
$.on($('a', msg), 'click', () => location.reload()); $.on($('a', msg), 'click', () => location.reload());
new Notice('warning', msg); new Notice('warning', msg);
$.crxWarningShown = true; $.crxWarningShown = true;
@ -644,16 +601,16 @@ if (platform === 'crx') {
return false; return false;
}; };
$.get = $.oneItemSugar(function(data, cb) { $.get = $.oneItemSugar(function (data, cb) {
if (!$.crxWorking()) { return; } if (!$.crxWorking()) { return; }
const results = {}; const results = {};
const get = function(area) { const get = function (area) {
let keys = Object.keys(data); let keys = Object.keys(data);
// XXX slow performance in Firefox // XXX slow performance in Firefox
if (($.engine === 'gecko') && (area === 'sync') && (keys.length > 3)) { if (($.engine === 'gecko') && (area === 'sync') && (keys.length > 3)) {
keys = null; keys = null;
} }
return chrome.storage[area].get(keys, function(result) { return chrome.storage[area].get(keys, function (result) {
let key; let key;
result = dict.clone(result); result = dict.clone(result);
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
@ -679,16 +636,16 @@ if (platform === 'crx') {
return get('sync'); return get('sync');
}); });
(function() { (function () {
const items = { const items = {
local: dict(), local: dict(),
sync: dict() sync: dict()
}; };
const exceedsQuota = (key, value) => // bytes in UTF-8 const exceedsQuota = (key, value) => // bytes in UTF-8
unescape(encodeURIComponent(JSON.stringify(key))).length + unescape(encodeURIComponent(JSON.stringify(value))).length > chrome.storage.sync.QUOTA_BYTES_PER_ITEM; unescape(encodeURIComponent(JSON.stringify(key))).length + unescape(encodeURIComponent(JSON.stringify(value))).length > chrome.storage.sync.QUOTA_BYTES_PER_ITEM;
$.delete = function(keys) { $.delete = function (keys) {
if (!$.crxWorking()) { return; } if (!$.crxWorking()) { return; }
if (typeof keys === 'string') { if (typeof keys === 'string') {
keys = [keys]; keys = [keys];
@ -702,11 +659,11 @@ if (platform === 'crx') {
}; };
const timeout = {}; const timeout = {};
var setArea = function(area, cb) { var setArea = function (area, cb) {
const data = dict(); const data = dict();
$.extend(data, items[area]); $.extend(data, items[area]);
if (!Object.keys(data).length || (timeout[area] > Date.now())) { return; } if (!Object.keys(data).length || (timeout[area] > Date.now())) { return; }
return chrome.storage[area].set(data, function() { return chrome.storage[area].set(data, function () {
let err; let err;
let key; let key;
if (err = chrome.runtime.lastError) { if (err = chrome.runtime.lastError) {
@ -738,20 +695,20 @@ if (platform === 'crx') {
var setSync = debounce(SECOND, () => setArea('sync')); var setSync = debounce(SECOND, () => setArea('sync'));
$.set = $.oneItemSugar(function(data, cb) { $.set = $.oneItemSugar(function (data, cb) {
if (!$.crxWorking()) { return; } if (!$.crxWorking()) { return; }
$.securityCheck(data); $.securityCheck(data);
$.extend(items.local, data); $.extend(items.local, data);
return setArea('local', cb); return setArea('local', cb);
}); });
return $.clear = function(cb) { return $.clear = function (cb) {
if (!$.crxWorking()) { return; } if (!$.crxWorking()) { return; }
items.local = dict(); items.local = dict();
items.sync = dict(); items.sync = dict();
let count = 2; let count = 2;
let err = null; let err = null;
const done = function() { const done = function () {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
c.error(chrome.runtime.lastError.message); c.error(chrome.runtime.lastError.message);
} }
@ -785,19 +742,20 @@ if (platform === 'crx') {
$.sync = (key, cb) => $.syncing[key] = cb; $.sync = (key, cb) => $.syncing[key] = cb;
$.forceSync = function() {}; $.forceSync = function () { };
$.delete = function(keys, cb) { $.delete = function (keys, cb) {
let key; let key;
if (!(keys instanceof Array)) { if (!(keys instanceof Array)) {
keys = [keys]; keys = [keys];
} }
return Promise.all((() => { return Promise.all((() => {
const result = []; const result = [];
for (key of keys) { result.push(GM.deleteValue(g.NAMESPACE + key)); for (key of keys) {
result.push(GM.deleteValue(g.NAMESPACE + key));
} }
return result; return result;
})()).then(function() { })()).then(function () {
const items = dict(); const items = dict();
for (key of keys) { items[key] = undefined; } for (key of keys) { items[key] = undefined; }
$.syncChannel.postMessage(items); $.syncChannel.postMessage(items);
@ -805,9 +763,9 @@ if (platform === 'crx') {
}); });
}; };
$.get = $.oneItemSugar(function(items, cb) { $.get = $.oneItemSugar(function (items, cb) {
const keys = Object.keys(items); const keys = Object.keys(items);
return Promise.all(keys.map((key) => GM.getValue(g.NAMESPACE + key))).then(function(values) { return Promise.all(keys.map((key) => GM.getValue(g.NAMESPACE + key))).then(function (values) {
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
var val = values[i]; var val = values[i];
if (val) { if (val) {
@ -818,7 +776,7 @@ if (platform === 'crx') {
}); });
}); });
$.set = $.oneItemSugar(function(items, cb) { $.set = $.oneItemSugar(function (items, cb) {
$.securityCheck(items); $.securityCheck(items);
return Promise.all((() => { return Promise.all((() => {
const result = []; const result = [];
@ -827,13 +785,13 @@ if (platform === 'crx') {
result.push(GM.setValue(g.NAMESPACE + key, JSON.stringify(val))); result.push(GM.setValue(g.NAMESPACE + key, JSON.stringify(val)));
} }
return result; return result;
})()).then(function() { })()).then(function () {
$.syncChannel.postMessage(items); $.syncChannel.postMessage(items);
return cb?.(); return cb?.();
}); });
}); });
$.clear = cb => GM.listValues().then(keys => $.delete(keys.map(key => key.replace(g.NAMESPACE, '')), cb)).catch( () => $.delete(Object.keys(Conf).concat(['previousversion', 'QR Size', 'QR.persona']), cb)); $.clear = cb => GM.listValues().then(keys => $.delete(keys.map(key => key.replace(g.NAMESPACE, '')), cb)).catch(() => $.delete(Object.keys(Conf).concat(['previousversion', 'QR Size', 'QR.persona']), cb));
} else { } else {
if (typeof GM_deleteValue === 'undefined' || GM_deleteValue === null) { if (typeof GM_deleteValue === 'undefined' || GM_deleteValue === null) {
@ -841,7 +799,7 @@ if (platform === 'crx') {
} }
if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) { if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) {
$.getValue = GM_getValue; $.getValue = GM_getValue;
$.listValues = () => GM_listValues(); // error when called if missing $.listValues = () => GM_listValues(); // error when called if missing
} else if ($.hasStorage) { } else if ($.hasStorage) {
$.getValue = key => localStorage.getItem(key); $.getValue = key => localStorage.getItem(key);
@ -855,23 +813,23 @@ if (platform === 'crx') {
return result; return result;
})(); })();
} else { } else {
$.getValue = function() {}; $.getValue = function () { };
$.listValues = () => []; $.listValues = () => [];
} }
if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) { if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) {
$.setValue = GM_setValue; $.setValue = GM_setValue;
$.deleteValue = GM_deleteValue; $.deleteValue = GM_deleteValue;
} else if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) { } else if (typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) {
$.oldValue = dict(); $.oldValue = dict();
$.setValue = function(key, val) { $.setValue = function (key, val) {
GM_setValue(key, val); GM_setValue(key, val);
if (key in $.syncing) { if (key in $.syncing) {
$.oldValue[key] = val; $.oldValue[key] = val;
if ($.hasStorage) { return localStorage.setItem(key, val); } // for `storage` events if ($.hasStorage) { return localStorage.setItem(key, val); } // for `storage` events
} }
}; };
$.deleteValue = function(key) { $.deleteValue = function (key) {
GM_deleteValue(key); GM_deleteValue(key);
if (key in $.syncing) { if (key in $.syncing) {
delete $.oldValue[key]; delete $.oldValue[key];
@ -881,37 +839,37 @@ if (platform === 'crx') {
if (!$.hasStorage) { $.cantSync = true; } if (!$.hasStorage) { $.cantSync = true; }
} else if ($.hasStorage) { } else if ($.hasStorage) {
$.oldValue = dict(); $.oldValue = dict();
$.setValue = function(key, val) { $.setValue = function (key, val) {
if (key in $.syncing) { $.oldValue[key] = val; } if (key in $.syncing) { $.oldValue[key] = val; }
return localStorage.setItem(key, val); return localStorage.setItem(key, val);
}; };
$.deleteValue = function(key) { $.deleteValue = function (key) {
if (key in $.syncing) { delete $.oldValue[key]; } if (key in $.syncing) { delete $.oldValue[key]; }
return localStorage.removeItem(key); return localStorage.removeItem(key);
}; };
} else { } else {
$.setValue = function() {}; $.setValue = function () { };
$.deleteValue = function() {}; $.deleteValue = function () { };
$.cantSync = ($.cantSet = true); $.cantSync = ($.cantSet = true);
} }
if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) { if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) {
$.sync = (key, cb) => $.syncing[key] = GM_addValueChangeListener(g.NAMESPACE + key, function(key2, oldValue, newValue, remote) { $.sync = (key, cb) => $.syncing[key] = GM_addValueChangeListener(g.NAMESPACE + key, function (key2, oldValue, newValue, remote) {
if (remote) { if (remote) {
if (newValue !== undefined) { newValue = dict.json(newValue); } if (newValue !== undefined) { newValue = dict.json(newValue); }
return cb(newValue, key); return cb(newValue, key);
} }
}); });
$.forceSync = function() {}; $.forceSync = function () { };
} else if ((typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) || $.hasStorage) { } else if ((typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) || $.hasStorage) {
$.sync = function(key, cb) { $.sync = function (key, cb) {
key = g.NAMESPACE + key; key = g.NAMESPACE + key;
$.syncing[key] = cb; $.syncing[key] = cb;
return $.oldValue[key] = $.getValue(key); return $.oldValue[key] = $.getValue(key);
}; };
(function() { (function () {
const onChange = function({key, newValue}) { const onChange = function ({ key, newValue }) {
let cb; let cb;
if (!(cb = $.syncing[key])) { return; } if (!(cb = $.syncing[key])) { return; }
if (newValue != null) { if (newValue != null) {
@ -926,20 +884,20 @@ if (platform === 'crx') {
}; };
$.on(window, 'storage', onChange); $.on(window, 'storage', onChange);
return $.forceSync = function(key) { return $.forceSync = function (key) {
// Storage events don't work across origins // Storage events don't work across origins
// e.g. http://boards.4chan.org and https://boards.4chan.org // e.g. http://boards.4chan.org and https://boards.4chan.org
// so force a check for changes to avoid lost data. // so force a check for changes to avoid lost data.
key = g.NAMESPACE + key; key = g.NAMESPACE + key;
return onChange({key, newValue: $.getValue(key)}); return onChange({ key, newValue: $.getValue(key) });
}; };
})(); })();
} else { } else {
$.sync = function() {}; $.sync = function () { };
$.forceSync = function() {}; $.forceSync = function () { };
} }
$.delete = function(keys) { $.delete = function (keys) {
if (!(keys instanceof Array)) { if (!(keys instanceof Array)) {
keys = [keys]; keys = [keys];
} }
@ -948,9 +906,9 @@ if (platform === 'crx') {
} }
}; };
$.get = $.oneItemSugar((items, cb) => $.queueTask($.getSync, items, cb)); $.get = $.oneItemSugar((items, cb) => $.queueTask(() => $.getSync(items, cb)));
$.getSync = function(items, cb) { $.getSync = function (items, cb) {
for (var key in items) { for (var key in items) {
var val2; var val2;
if (val2 = $.getValue(g.NAMESPACE + key)) { if (val2 = $.getValue(g.NAMESPACE + key)) {
@ -967,9 +925,9 @@ if (platform === 'crx') {
return cb(items); return cb(items);
}; };
$.set = $.oneItemSugar(function(items, cb) { $.set = $.oneItemSugar(function (items, cb) {
$.securityCheck(items); $.securityCheck(items);
return $.queueTask(function() { return $.queueTask(function () {
for (var key in items) { for (var key in items) {
var value = items[key]; var value = items[key];
$.setValue(g.NAMESPACE + key, JSON.stringify(value)); $.setValue(g.NAMESPACE + key, JSON.stringify(value));
@ -978,14 +936,14 @@ if (platform === 'crx') {
}); });
}); });
$.clear = function(cb) { $.clear = function (cb) {
// XXX https://github.com/greasemonkey/greasemonkey/issues/2033 // XXX https://github.com/greasemonkey/greasemonkey/issues/2033
// Also support case where GM_listValues is not defined. // Also support case where GM_listValues is not defined.
$.delete(Object.keys(Conf)); $.delete(Object.keys(Conf));
$.delete(['previousversion', 'QR Size', 'QR.persona']); $.delete(['previousversion', 'QR Size', 'QR.persona']);
try { try {
$.delete($.listValues().map(key => key.replace(g.NAMESPACE, ''))); $.delete($.listValues().map(key => key.replace(g.NAMESPACE, '')));
} catch (error) {} } catch (error) { }
return cb?.(); return cb?.();
}; };
} }