Update
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name EH Gallery Script
|
||||
// @namespace https://github.com/lifegpc/userscript
|
||||
// @version 0.1.3
|
||||
// @version 0.1.4
|
||||
// @description :(
|
||||
// @author lifegpc
|
||||
// @match https://*.e-hentai.org/g/*/*
|
||||
@@ -25,15 +25,240 @@
|
||||
// @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: {
|
||||
@@ -51,6 +276,11 @@ GM_config.init({
|
||||
type: 'checkbox',
|
||||
label: 'Open Multi-Page Viewer in new tab.',
|
||||
default: true
|
||||
},
|
||||
enableTagTranslation: {
|
||||
type: 'checkbox',
|
||||
label: 'Enable tag translation.',
|
||||
default: false
|
||||
}
|
||||
},
|
||||
events: {
|
||||
@@ -68,14 +298,62 @@ GM_config.init({
|
||||
ele.target = openMPVInNewTab ? '_blank' : '_self';
|
||||
}
|
||||
}
|
||||
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");
|
||||
let observer = new MutationObserver((data) => {
|
||||
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') {
|
||||
@@ -87,6 +365,239 @@ let observer = new MutationObserver((data) => {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(document, { childList: true, subtree: true });
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user