From dd952892b8820e5ca10a55359cc6e4b9d8b04920 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sat, 9 Mar 2024 12:00:27 +0800 Subject: [PATCH] Update --- eh_gallery_script.user.js | 515 +++++++++++++++++++++++++++++++++++++- 1 file changed, 513 insertions(+), 2 deletions(-) diff --git a/eh_gallery_script.user.js b/eh_gallery_script.user.js index 9d97f34..2d6b4d5 100644 --- a/eh_gallery_script.user.js +++ b/eh_gallery_script.user.js @@ -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} +*/ +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, 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} + */ +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 = $(`
${msg}
`) + $('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"); + } + } + } + } +}