610 lines
23 KiB
JavaScript
610 lines
23 KiB
JavaScript
// ==UserScript==
|
|
// @name EH Gallery Script
|
|
// @namespace https://github.com/lifegpc/userscript
|
|
// @version 0.1.5
|
|
// @description :(
|
|
// @author lifegpc
|
|
// @match https://*.e-hentai.org/g/*/*
|
|
// @match https://*.e-hentai.org/
|
|
// @match https://*.e-hentai.org/?*
|
|
// @match https://*.e-hentai.org/watched
|
|
// @match https://*.e-hentai.org/watched?*
|
|
// @match https://*.e-hentai.org/popular
|
|
// @match https://*.e-hentai.org/popular?*
|
|
// @match https://*.e-hentai.org/favorites.php
|
|
// @match https://*.e-hentai.org/favorites.php?*
|
|
// @match https://*.e-hentai.org/tag/*
|
|
// @match https://*.exhentai.org/g/*/*
|
|
// @match https://*.exhentai.org/
|
|
// @match https://*.exhentai.org/?*
|
|
// @match https://*.exhentai.org/watched
|
|
// @match https://*.exhentai.org/watched?*
|
|
// @match https://*.exhentai.org/popular
|
|
// @match https://*.exhentai.org/popular?*
|
|
// @match https://*.exhentai.org/favorites.php
|
|
// @match https://*.exhentai.org/favorites.php?*
|
|
// @match https://*.exhentai.org/tag/*
|
|
// @icon https://e-hentai.org/favicon.ico
|
|
// @grant GM_addStyle
|
|
// @grant GM_getResourceText
|
|
// @grant GM_getValue
|
|
// @grant GM_setValue
|
|
// @grant GM_registerMenuCommand
|
|
// @grant GM_xmlhttpRequest
|
|
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
|
|
// @require https://cdn.jsdelivr.net/npm/marked
|
|
// @require https://cdn.jsdelivr.net/npm/jquery
|
|
// @require https://unpkg.com/@popperjs/core@2
|
|
// @require https://unpkg.com/tippy.js@6
|
|
// @resource s https://unpkg.com/tippy.js@6/themes/light-border.css
|
|
// @run-at document-start
|
|
// @connect raw.githubusercontent.com
|
|
// ==/UserScript==
|
|
function replaceAll(s, pattern, replacement) {
|
|
return s.split(pattern).join(replacement);
|
|
}
|
|
const SVG_ICON_MSG = "data:image/svg+xml,%3Csvg t='1595842925125' class='icon' viewBox='0 0 1024 1024' version='1.1' xmlns='http://www.w3.org/2000/svg' p-id='2280' width='200' height='200'%3E%3Cpath d='M89.216226 575.029277c-6.501587-7.223986-10.47478-15.892769-12.641975-26.367549-1.805996-10.47478-0.722399-20.22716 3.973192-29.257143l4.695591-10.47478c5.05679-8.307584 11.558377-13.725573 19.865961-15.892769 7.946384-2.167196 15.892769-0.361199 23.477954 5.417989L323.995767 639.322751c8.307584 5.779189 17.698765 8.668783 27.812346 8.307584 10.11358-0.361199 18.782363-3.611993 26.006349-10.11358L898.302646 208.411993c7.585185-5.779189 16.253968-8.307584 26.006349-7.585185 9.752381 0.722399 18.059965 4.334392 24.922751 10.47478l-12.641975-12.641975c6.501587 7.223986 9.752381 15.17037 9.752381 24.561552 0 9.391182-3.250794 17.337566-9.752381 24.561552L376.008466 816.310406c-7.223986 7.223986-15.17037 10.47478-24.200353 10.47478-9.029982 0-16.976367-3.250794-24.200353-9.752381L89.216226 575.029277z' p-id='2281' fill='%23ffffff'%3E%3C/path%3E%3C/svg%3E";
|
|
const GALLERY_REG = /e[-x]hentai\.org\/g\/\d+\/[^/]+/;
|
|
const IMG_REG = /e[-x]hentai\.org\/s\/[^/]+\/\d+-\d+/;
|
|
const MPV_REG = /e[-x]hentai\.org\/mpv\/\d+\/[^/]+/;
|
|
/**@type {IDBDatabase} */
|
|
let db = undefined;
|
|
let need_reinit = false;
|
|
let storage = navigator.storage || globalThis['WorkerNavigator']['storage'];
|
|
const BLACK_LIST_HOST = [
|
|
"tva1.sinaimg.cn",
|
|
"img.moegirl.org.cn",
|
|
];
|
|
|
|
function filter_html(html) {
|
|
let doc = (new DOMParser).parseFromString(html, "text/html");
|
|
doc.querySelectorAll('a').forEach(a => a.setAttribute('target', '_blank'));
|
|
doc.querySelectorAll('img[src="#"]').forEach((img) => {
|
|
let title = img.title;
|
|
try {
|
|
new URL(title);
|
|
img.src = title;
|
|
img.title = "";
|
|
} catch (_) {
|
|
img.remove();
|
|
}
|
|
})
|
|
doc.querySelectorAll('img').forEach((img) => {
|
|
try {
|
|
let u = new URL(img.src);
|
|
if (BLACK_LIST_HOST.includes(u.host)) {
|
|
console.log("Remove blacklist URL: ", u.toString());
|
|
img.remove();
|
|
}
|
|
} catch (_) {}
|
|
})
|
|
return doc.body.innerHTML;
|
|
}
|
|
function filter_html2(html) {
|
|
let doc = (new DOMParser).parseFromString(html, "text/html");
|
|
doc.querySelectorAll('img[src="#"]').forEach((img) => {
|
|
let title = img.title;
|
|
try {
|
|
new URL(title);
|
|
img.src = title;
|
|
img.title = "";
|
|
} catch (_) {
|
|
img.remove();
|
|
}
|
|
})
|
|
doc.querySelectorAll('img').forEach((img) => {
|
|
img.style.height = "12px";
|
|
})
|
|
return doc.body.innerHTML;
|
|
}
|
|
async function make_storage_persist() {
|
|
let persisted = await storage.persisted();
|
|
if (!persisted) {
|
|
persisted = await storage.persist();
|
|
}
|
|
return persisted;
|
|
}
|
|
function init() {
|
|
return new Promise((resolve, reject) => {
|
|
make_storage_persist();
|
|
if (db !== undefined && !need_reinit) {
|
|
resolve();
|
|
return;
|
|
}
|
|
let indexedReq = indexedDB.open('eh_tags', 1);
|
|
/**@param {IDBVersionChangeEvent} event*/
|
|
indexedReq.onupgradeneeded = function (event) {
|
|
let db = this.result;
|
|
console.log(`upgrade eh_tags from ${event.oldVersion} to ${event.newVersion}`);
|
|
/*No version or version < 1 -> v1 */
|
|
if (isNaN(event.oldVersion) || event.oldVersion < 1) {
|
|
db.createObjectStore('meta', { keyPath: 'key' })
|
|
db.createObjectStore('tags', { keyPath: 'tag' });
|
|
}
|
|
}
|
|
indexedReq.onsuccess = () => {
|
|
db = indexedReq.result;
|
|
resolve();
|
|
}
|
|
indexedReq.onerror = () => {
|
|
need_reinit = true;
|
|
reject(indexedReq.error);
|
|
}
|
|
})
|
|
}
|
|
/**@param {string} key*/
|
|
function get_value(key) {
|
|
return new Promise((resolve, reject) => {
|
|
init().then(() => {
|
|
let tx = db.transaction(['meta'], 'readonly');
|
|
let store = tx.objectStore('meta');
|
|
let req = store.get(key);
|
|
req.onsuccess = () => {
|
|
let re = req.result;
|
|
resolve(re ? re.value : undefined);
|
|
}
|
|
req.onerror = () => {
|
|
need_reinit = true;
|
|
reject(req.error);
|
|
}
|
|
}).catch(reject);
|
|
})
|
|
}
|
|
/**@param {string} key*/
|
|
function set_value(key, value) {
|
|
return new Promise((resolve, reject) => {
|
|
init().then(() => {
|
|
let tx = db.transaction(['meta'], 'readwrite');
|
|
let store = tx.objectStore('meta');
|
|
let req = store.put({ key, value });
|
|
req.onsuccess = () => {
|
|
resolve();
|
|
}
|
|
req.onerror = () => {
|
|
need_reinit = true;
|
|
reject(req.error);
|
|
}
|
|
}).catch(reject);
|
|
})
|
|
}
|
|
/**
|
|
* @typedef {{intro: string, links: string, name: string}} TagData
|
|
* @param {string} tag
|
|
* @param {TagData} value
|
|
*/
|
|
function set_tag(tag, value) {
|
|
return new Promise((resolve, reject) => {
|
|
init().then(() => {
|
|
let tx = db.transaction(['tags'], 'readwrite');
|
|
let store = tx.objectStore('tags');
|
|
let req = store.put({ tag, value });
|
|
req.onsuccess = () => {
|
|
resolve();
|
|
}
|
|
req.onerror = () => {
|
|
need_reinit = true;
|
|
reject(req.error);
|
|
}
|
|
}).catch(reject);
|
|
})
|
|
}
|
|
/**
|
|
* @param {string} tag
|
|
* @returns {Promise<TagData | undefined>}
|
|
*/
|
|
function get_tag(tag) {
|
|
return new Promise((resolve, reject) => {
|
|
init().then(() => {
|
|
let tx = db.transaction(['tags'], 'readonly');
|
|
let store = tx.objectStore('tags');
|
|
let req = store.get(tag);
|
|
req.onsuccess = () => {
|
|
let re = req.result;
|
|
resolve(re ? re.value : undefined);
|
|
}
|
|
req.onerror = () => {
|
|
need_reinit = true;
|
|
reject(req.error);
|
|
}
|
|
}).catch(reject);
|
|
})
|
|
}
|
|
/**
|
|
* @typedef {{method?: string, headers?: Record<string, string>, data?: string|Blob|File|FormData|URLSearchParams, redirect?: RequestRedirect}} FetchOptions
|
|
* @typedef {{finalUrl: string, readyState: number, status: number, statusText: string, responseHeaders: string, response: any, responseText: string}} FetchResponse
|
|
* @param {string|URL} url
|
|
* @param {FetchOptions | undefined} options
|
|
* @returns {Promise<FetchResponse>}
|
|
*/
|
|
function GM_fetch(url, options = undefined) {
|
|
XMLHttpRequest
|
|
return new Promise((resolve, reject) => {
|
|
GM_xmlhttpRequest({
|
|
url,
|
|
method: options?.method ?? "GET",
|
|
headers: options?.headers,
|
|
data: options?.data,
|
|
redirect: options?.redirect,
|
|
onabort: () => {
|
|
reject("aborted.");
|
|
},
|
|
onerror: (err) => {
|
|
reject(err);
|
|
},
|
|
onload: (re) => {
|
|
resolve(re);
|
|
}
|
|
})
|
|
})
|
|
}
|
|
async function fetch_tags() {
|
|
const uri = "https://raw.githubusercontent.com/EhTagTranslation/DatabaseReleases/master/db.raw.json";
|
|
const re = await GM_fetch(uri, { headers: { "Cache-Control": "no-cache" } });
|
|
if (re.status != 200) {
|
|
throw new Error(`Failed to fetch tags: ${re.status} ${re.statusText}`);
|
|
}
|
|
let data = JSON.parse(re.responseText);
|
|
await set_value("last_updated", Date.now());
|
|
const head = await get_value("tags_head");
|
|
if (head?.sha == data.head.sha) {
|
|
console.log("Same sha, skip update tags.");
|
|
return;
|
|
}
|
|
await set_value("tags_head", data.head);
|
|
for (const namespace of data.data) {
|
|
const ns = namespace.namespace;
|
|
for (const tag in namespace.data) {
|
|
const value = namespace.data[tag];
|
|
await set_tag(`${ns}:${tag}`, value);
|
|
}
|
|
}
|
|
}
|
|
GM_config.init({
|
|
id: 'e-hentai',
|
|
fields: {
|
|
openImageInNewTab: {
|
|
type: 'checkbox',
|
|
label: 'Open image in new tab.',
|
|
default: true
|
|
},
|
|
openGalleryInNewTab: {
|
|
type: 'checkbox',
|
|
label: 'Open gallery in new tab.',
|
|
default: true
|
|
},
|
|
openMPVInNewTab: {
|
|
type: 'checkbox',
|
|
label: 'Open Multi-Page Viewer in new tab.',
|
|
default: true
|
|
},
|
|
enableTagTranslation: {
|
|
type: 'checkbox',
|
|
label: 'Enable tag translation.',
|
|
default: false
|
|
}
|
|
},
|
|
events: {
|
|
save: (values) => {
|
|
handle_doc();
|
|
}
|
|
}
|
|
});
|
|
async function handle_doc() {
|
|
let openImageInNewTab = GM_config.get("openImageInNewTab");
|
|
let openGalleryInNewTab = GM_config.get("openGalleryInNewTab");
|
|
let openMPVInNewTab = GM_config.get("openMPVInNewTab");
|
|
let eles = document.getElementsByTagName("a");
|
|
for (let ele of eles) {
|
|
if (ele.href.match(IMG_REG)) {
|
|
ele.target = openImageInNewTab ? '_blank' : '_self';
|
|
} else if (ele.href.match(GALLERY_REG)) {
|
|
ele.target = openGalleryInNewTab ? '_blank' : '_self';
|
|
} else if (ele.href.match(MPV_REG)) {
|
|
ele.target = openMPVInNewTab ? '_blank' : '_self';
|
|
}
|
|
}
|
|
await handle_tags();
|
|
}
|
|
/**@param {MsgType} level */
|
|
function err_handle(e, basic = "", level = "err") {
|
|
console.error(e);
|
|
let msg = "";
|
|
if (typeof e === "string") msg = e;
|
|
if (e instanceof Error) msg = e.message;
|
|
popMsg(`${basic}${msg}`, level);
|
|
}
|
|
GM_registerMenuCommand("Edit Settings", () => { GM_config.open() }, "e");
|
|
GM_registerMenuCommand("Update translation data", () => {
|
|
fetch_tags().then(() => {
|
|
popMsg("Update translation data successful.");
|
|
}).catch((e) => {
|
|
err_handle(e, "Update translation data failed: ");
|
|
})
|
|
}, "u");
|
|
GM_addStyle(`
|
|
.egs__msg{display:none;position:fixed;top:10px;left:50%;transform:translateX(-50%);color:#fff;text-align:center;z-index:99996;padding:10px 30px 10px 45px;font-size:16px;border-radius:10px;background-image:url("${SVG_ICON_MSG}");background-size:25px;background-repeat:no-repeat;background-position:15px}
|
|
.egs__msg a{color:#fff;text-decoration: underline;}
|
|
.egs__msg-ok{background:#4bcc4b}
|
|
.egs__msg-err{background:#c33}
|
|
.egs__msg-warn{background:#FF9900}
|
|
td.tc{text-wrap:nowrap}
|
|
`)
|
|
GM_addStyle(GM_getResourceText("s"));
|
|
/**
|
|
* @param {string} msg
|
|
* @typedef {'ok'|'err'|'warn'} MsgType
|
|
* @param {MsgType} type
|
|
*/
|
|
function popMsg(msg, type = "ok") {
|
|
$('.egs__msg').length > 0 && $('.egs__msg').remove()
|
|
let $msg = $(`<div class="egs__msg egs__msg-${type}">${msg}</div>`)
|
|
$('body').append($msg)
|
|
$msg.slideDown(200)
|
|
setTimeout(() => { $msg.fadeOut(500) }, type == 'ok' ? 2000 : 5000)
|
|
setTimeout(() => { $msg.remove() }, type == 'ok' ? 2500 : 5500)
|
|
}
|
|
const instances = {};
|
|
let insid = 0;
|
|
function set_instance(ins) {
|
|
instances[insid] = ins;
|
|
return insid++;
|
|
}
|
|
let observer = new MutationObserver(async (data) => {
|
|
let openImageInNewTab = GM_config.get("openImageInNewTab");
|
|
let openGalleryInNewTab = GM_config.get("openGalleryInNewTab");
|
|
let openMPVInNewTab = GM_config.get("openMPVInNewTab");
|
|
let enableTagTranslation = GM_config.get("enableTagTranslation");
|
|
if (enableTagTranslation) {
|
|
await check_update();
|
|
}
|
|
for (let i of data) {
|
|
let ele = i.target;
|
|
if (ele.tagName == 'A') {
|
|
if (ele.href.match(IMG_REG)) {
|
|
ele.target = openImageInNewTab ? '_blank' : '_self';
|
|
} else if (ele.href.match(GALLERY_REG)) {
|
|
ele.target = openGalleryInNewTab ? '_blank' : '_self';
|
|
} else if (ele.href.match(MPV_REG)) {
|
|
ele.target = openMPVInNewTab ? '_blank' : '_self';
|
|
}
|
|
}
|
|
if (ele.tagName == 'TD') {
|
|
/**@type {HTMLTableCellElement} */
|
|
const e = ele;
|
|
if (e.classList.contains("tc")) {
|
|
if (enableTagTranslation) {
|
|
let tag = e.getAttribute('otag');
|
|
if (tag) continue;
|
|
const t = e.textContent.split(":")[0];
|
|
const value = await get_tag(`rows:${t}`);
|
|
e.setAttribute('otag', t);
|
|
if (value) {
|
|
e.textContent = `${value.name}:`;
|
|
let html = "";
|
|
if (value.intro) {
|
|
html += marked.parse(value.intro);
|
|
}
|
|
if (value.links) {
|
|
html += marked.parse(value.links);
|
|
}
|
|
if (html) {
|
|
e.setAttribute("tippy-id", set_instance(tippy(e, {
|
|
content: filter_html(html),
|
|
allowHTML: true,
|
|
interactive: true,
|
|
theme: 'light-border',
|
|
placement: 'right-start',
|
|
maxWidth: 400,
|
|
delay: [500, 0]
|
|
})));
|
|
}
|
|
}
|
|
} else {
|
|
let tag = e.getAttribute('otag');
|
|
if (tag) {
|
|
e.textContent = `${tag}:`;
|
|
e.removeAttribute('otag');
|
|
let id = e.getAttribute("tippy-id");
|
|
if (id) {
|
|
instances[id].destroy();
|
|
delete instances[id];
|
|
e.removeAttribute("tippy-id");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (ele.tagName == 'DIV') {
|
|
/**@type {HTMLDivElement} */
|
|
const e = ele;
|
|
const is_tag = Array.from(e.classList.entries()).findIndex((v) => v[1].startsWith("gt")) != -1;
|
|
if (is_tag) {
|
|
if (enableTagTranslation) {
|
|
let otag = e.getAttribute('otag');
|
|
if (otag) continue;
|
|
const t = e.id ? replaceAll(e.id.split("td_")[1], '_', ' ') : e.title;
|
|
if (!t) continue;
|
|
const value = await get_tag(t);
|
|
e.setAttribute('otag', t);
|
|
if (value) {
|
|
const name = filter_html2(replaceAll(marked.parse(value.name), /<\/?p>/, ''));
|
|
if (e.id) {
|
|
e.children[0].innerHTML = name;
|
|
} else {
|
|
e.innerHTML = name;
|
|
}
|
|
let html = "";
|
|
if (value.intro) {
|
|
html += marked.parse(value.intro);
|
|
}
|
|
if (value.links) {
|
|
html += marked.parse(value.links);
|
|
}
|
|
if (html) {
|
|
e.setAttribute("tippy-id", set_instance(tippy(e, {
|
|
content: filter_html(html),
|
|
allowHTML: true,
|
|
interactive: true,
|
|
theme: 'light-border',
|
|
placement: 'right-start',
|
|
maxWidth: 400,
|
|
delay: [500, 0]
|
|
})));
|
|
}
|
|
}
|
|
} else {
|
|
let tag = e.getAttribute('otag');
|
|
if (tag) {
|
|
let otag = tag.split(":")[1];
|
|
if (e.id) {
|
|
e.children[0].innerText = otag;
|
|
} else {
|
|
e.textContent = otag;
|
|
}
|
|
e.removeAttribute('otag');
|
|
let id = e.getAttribute("tippy-id");
|
|
if (id) {
|
|
instances[id].destroy();
|
|
delete instances[id];
|
|
e.removeAttribute("tippy-id");
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
let checked_update = false;
|
|
let is_checking_update = false;
|
|
async function check_tags_update() {
|
|
let last_updated = await get_value("last_updated");
|
|
console.log("Last updated: ", new Date(last_updated))
|
|
if (!last_updated || Date.now() - last_updated > 1000 * 60 * 60 * 24) {
|
|
await fetch_tags();
|
|
}
|
|
checked_update = true;
|
|
}
|
|
function check_update() {
|
|
return new Promise((resolve, reject) => {
|
|
if (checked_update || is_checking_update) {
|
|
resolve();
|
|
return;
|
|
}
|
|
is_checking_update = true;
|
|
check_tags_update().then(resolve).catch((e) => {
|
|
err_handle(e, "Failed to check update: ", "warn");
|
|
resolve();
|
|
}).finally(() => {
|
|
is_checking_update = false;
|
|
});
|
|
})
|
|
}
|
|
async function handle_tags() {
|
|
let enableTagTranslation = GM_config.get("enableTagTranslation");
|
|
if (enableTagTranslation) {
|
|
await check_update();
|
|
}
|
|
let groups = document.querySelectorAll("td.tc");
|
|
for (let group of groups) {
|
|
if (enableTagTranslation) {
|
|
let tag = group.getAttribute('otag');
|
|
if (tag) continue;
|
|
const t = group.textContent.split(":")[0];
|
|
const value = await get_tag(`rows:${t}`);
|
|
group.setAttribute('otag', t);
|
|
if (value) {
|
|
group.textContent = `${value.name}:`;
|
|
let html = "";
|
|
if (value.intro) {
|
|
html += marked.parse(value.intro);
|
|
}
|
|
if (value.links) {
|
|
html += marked.parse(value.links);
|
|
}
|
|
if (html) {
|
|
group.setAttribute("tippy-id", set_instance(tippy(group, {
|
|
content: filter_html(html),
|
|
allowHTML: true,
|
|
interactive: true,
|
|
theme: 'light-border',
|
|
placement: 'right-start',
|
|
maxWidth: 400,
|
|
delay: [500, 0]
|
|
})));
|
|
}
|
|
}
|
|
} else {
|
|
let tag = group.getAttribute('otag');
|
|
if (tag) {
|
|
group.textContent = `${tag}:`;
|
|
group.removeAttribute('otag');
|
|
let id = group.getAttribute("tippy-id");
|
|
if (id) {
|
|
instances[id].destroy();
|
|
delete instances[id];
|
|
group.removeAttribute("tippy-id");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let tags = document.querySelectorAll("div[class^=gt]");
|
|
for (const i of tags) {
|
|
if (enableTagTranslation) {
|
|
let otag = i.getAttribute('otag');
|
|
if (otag) continue;
|
|
const t = i.id ? replaceAll(e.id.split("td_")[1], '_', ' ') : i.title;
|
|
if (!t) continue;
|
|
const value = await get_tag(t);
|
|
i.setAttribute('otag', t);
|
|
if (value) {
|
|
const name = filter_html2(replaceAll(marked.parse(value.name), /<\/?p>/, ''));
|
|
if (i.id) {
|
|
i.children[0].innerHTML = name;
|
|
} else {
|
|
i.innerHTML = name;
|
|
}
|
|
let html = "";
|
|
if (value.intro) {
|
|
html += marked.parse(value.intro);
|
|
}
|
|
if (value.links) {
|
|
html += marked.parse(value.links);
|
|
}
|
|
if (html) {
|
|
i.setAttribute("tippy-id", set_instance(tippy(i, {
|
|
content: filter_html(html),
|
|
allowHTML: true,
|
|
interactive: true,
|
|
theme: 'light-border',
|
|
placement: 'right-start',
|
|
maxWidth: 400,
|
|
delay: [500, 0]
|
|
})));
|
|
}
|
|
}
|
|
} else {
|
|
let tag = i.getAttribute('otag');
|
|
if (tag) {
|
|
let otag = tag.split(":")[1];
|
|
if (i.id) {
|
|
i.children[0].innerText = otag;
|
|
} else {
|
|
i.textContent = otag;
|
|
}
|
|
i.removeAttribute('otag');
|
|
let id = i.getAttribute("tippy-id");
|
|
if (id) {
|
|
instances[id].destroy();
|
|
delete instances[id];
|
|
i.removeAttribute("tippy-id");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
window.addEventListener('DOMContentLoaded', async () => {
|
|
handle_doc();
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
})
|