您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
色花堂支持帖外预览图片
// ==UserScript== // @name FORUM-JHS // @namespace https://sleazyfork.org/zh-CN/scripts/549737-forum-jhs // @version 0.0.1 // @author xie bro // @description 色花堂支持帖外预览图片 // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=sehuatang.net // @include https://*sehuatang.*/* // @require https://update.greasyfork.org/scripts/540597/1613170/parallel_GM_xmlhttpRequest.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.js // @connect * // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant unsafeWindow // ==/UserScript== var e = Object.defineProperty, t = (t, r, n) => ((t, r, n) => r in t ? e(t, r, { enumerable: !0, configurable: !0, writable: !0, value: n }) : t[r] = n)(t, "symbol" != typeof r ? r + "" : r, n); class r { constructor() { return t(this, "intervalContainer", {}), t(this, "insertStyle", (e => { e && (-1 === e.indexOf("<style>") && (e = "<style>" + e + "</style>"), $("head").append(e)); })), r.instance || (r.instance = this), r.instance; } importResource(e) { let t; e.indexOf("css") >= 0 ? (t = document.createElement("link"), t.setAttribute("rel", "stylesheet"), t.href = e) : (t = document.createElement("script"), t.setAttribute("type", "text/javascript"), t.src = e), document.documentElement.appendChild(t); } loopDetector(e, t, r = 20, n = 1e4, s = !0) { let o = !1; const i = Math.random(), a = (new Date).getTime(); this.intervalContainer[i] = setInterval((() => { (new Date).getTime() - a > n && (console.warn("loopDetector timeout!", e, t), o = s), (e() || o) && (clearInterval(this.intervalContainer[i]), t && t(), delete this.intervalContainer[i]); }), r); } } window.utils = new r, window.http = new class { get(e, t = {}, r = {}) { return this.jqueryRequest("GET", e, null, t, r); } post(e, t = {}, r = {}) { return this.jqueryRequest("POST", e, t, null, r); } put(e, t = {}, r = {}) { return this.jqueryRequest("PUT", e, t, null, r); } del(e, t = {}, r = {}) { return this.jqueryRequest("DELETE", e, null, t, r); } jqueryRequest(e, t, r = {}, n = {}, s = {}) { return "POST" === e && (s = { "Content-Type": "application/json", ...s }), new Promise(((o, i) => { $.ajax({ method: e, url: t, timeout: 1e4, data: "GET" === e || "DELETE" === e ? n : JSON.stringify(r), headers: s, success: (e, t, r) => { var n; if (null == (n = r.getResponseHeader("Content-Type")) ? void 0 : n.includes("application/json")) try { o("object" == typeof e ? e : JSON.parse(e)); } catch (s) { o(e); } else o(e); }, error: (e, t, r) => { let n = r; if (e.responseText) try { const t = JSON.parse(e.responseText); n = t.message || t.msg || e.responseText; } catch { n = e.responseText; } i(new Error(n)); } }); })); } }, window.gmHttp = new class { get(e, t = {}, r = {}, n) { return this.gmRequest("GET", e, null, t, r, n); } post(e, t = {}, r = {}, n) { r = { "Content-Type": "application/json", ...r }; let s = JSON.stringify(t); return this.gmRequest("POST", e, s, null, r, n); } postForm(e, t = {}, r = {}, n) { r || (r = {}), r["Content-Type"] || (r["Content-Type"] = "application/x-www-form-urlencoded"); let s = ""; return t && Object.keys(t).length > 0 && (s = Object.entries(t).map((([e, t]) => `${e}=${t}`)).join("&")), this.gmRequest("POST", e, s, null, r, n); } postFormData(e, t = {}, r = {}, n) { r || (r = {}); const s = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`; r["Content-Type"] = `multipart/form-data; boundary=${s}`; let o = ""; return t && Object.keys(t).length > 0 && (o = Object.entries(t).map((([e, t]) => `--${s}\r\nContent-Disposition: form-data; name="${e}"\r\n\r\n${t}\r\n`)).join("")), o += `--${s}--`, this.gmRequest("POST", e, o, null, r, n); } checkUrlStatus(e, t = {}, r) { return new Promise(((n, s) => { GM_xmlhttpRequest({ method: "HEAD", url: e, headers: t, timeout: r || 1e4, onload: e => { n(e.status); }, onerror: e => { s(new Error(`请求失败: ${e}`)); }, ontimeout: () => { s(new Error(`请求超时(${r}ms)`)); } }); })); } gmRequest(e, t, r = {}, n = {}, s = {}, o) { if (n && Object.keys(n).length) { const e = new URLSearchParams(n).toString(); t += (t.includes("?") ? "&" : "?") + e; } return new Promise(((n, i) => { GM_xmlhttpRequest({ method: e, url: t, headers: s, timeout: o || 1e4, data: r, onload: e => { try { if (e.status >= 200 && e.status < 300) if (e.responseText) try { n(JSON.parse(e.responseText)); } catch (r) { n(e.responseText); } else n(e.responseText || e); else if (console.error("请求失败,状态码:", e.status, t), e.responseText) try { const t = JSON.parse(e.responseText); i(t); } catch { i(new Error(e.responseText || `HTTP Error ${e.status}`)); } else i(new Error(`HTTP Error ${e.status}`)); } catch (r) { i(r); } }, onerror: e => { console.error("网络错误:", t), i(new Error(e.error || "Network Error")); }, ontimeout: () => { i(new Error("Request Timeout")); } }); })); } }, function() { const e = (e, t, r, n, s) => { let o; "object" == typeof r ? o = r : (o = "object" == typeof n ? n : s || {}, o.gravity = r || "top", o.position = "string" == typeof n ? n : "center"), o.gravity && "center" !== o.gravity || (o.offset = { y: "calc(50vh - 150px)" }); const i = "#60A5FA", a = "#93C5FD", c = "#10B981", l = "#6EE7B7", u = "#EF4444", p = "#FCA5A5", m = { borderRadius: "12px", color: "white", padding: "12px 16px", boxShadow: "0 4px 6px rgba(0,0,0,0.1)", minWidth: "150px", textAlign: "center", zIndex: 999999999 }, g = { text: e, duration: 2e3, close: !1, gravity: "top", position: "center", style: { info: { ...m, background: `linear-gradient(to right, ${i}, ${a})` }, success: { ...m, background: `linear-gradient(to right, ${c}, ${l})` }, error: { ...m, background: `linear-gradient(to right, ${u}, ${p})` } }[t], stopOnFocus: !0, oldestFirst: !1, ...o }; -1 === g.duration && (g.close = !0); const d = Toastify(g); return d.showToast(), d.closeShow = () => { d.toastElement.remove(); }, d; }; window.show = { ok: (t, r = "center", n, s) => e(t, "success", r, n, s), error: (t, r = "center", n, s) => e(t, "error", r, n, s), info: (t, r = "center", n, s) => e(t, "info", r, n, s) }; }(); class n { constructor() { t(this, "pluginManager", null); } getName() { throw new Error(`${this.constructor.name} 未显示getName()`); } getBean(e) { let t = this.pluginManager.getBean(e); if (!t) { let t = "容器中不存在: " + e; throw show.error(t), new Error(t); } return t; } async initCss() { return ""; } async handle() {} } utils.importResource("https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.css"); const s = new class { constructor() { this.plugins = new Map; } register(e) { if ("function" != typeof e) throw new Error("插件必须是一个类"); const t = new e; t.pluginManager = this; const r = t.getName().toLowerCase(); if (this.plugins.has(r)) throw new Error(`插件"${name}"已注册`); this.plugins.set(r, t); } getBean(e) { return this.plugins.get(e.toLowerCase()); } _getDependencies(e) { const t = e.toString(); return t.slice(t.indexOf("(") + 1, t.indexOf(")")).split(",").map((e => e.trim())).filter((e => e)); } async process() { const e = (await Promise.allSettled(Array.from(this.plugins).map((async ([e, t]) => { try { if ("function" == typeof t.handle) { const r = await t.initCss(); return utils.insertStyle(r), await t.handle(), { name: e, status: "fulfilled" }; } console.log("加载插件", e); } catch (r) { return console.error(`插件 ${e} 执行失败`, r), { name: e, status: "rejected", error: r }; } })))).filter((e => "rejected" === e.status)); e.length && console.error("以下插件执行失败:", e.map((e => e.name))), document.body.classList.add("script-ready"); } }; s.register(class extends n { constructor() { super(...arguments), t(this, "currentImageIndex", 0), t(this, "currentImageGroup", []), t(this, "processedArticles", new Set); } getName() { return "SeHuaTangPlugin"; } async initCss() { return "\n <style>\n /*.icn{\n width: 85px !important;\n }*/\n .xst{\n font-size: 15px;\n color: #090909;\n }\n #threadlisttableid em{\n font-size: 15px;\n }\n </style>\n "; } async handle() { let e = $(".enter-btn"); e.length > 0 && e[0].click(), window.location.href.includes("viewthread") || (utils.loopDetector((() => $(".s.xst").length > 0), (() => { this.parseArticleImg().then(); })), utils.loopDetector((() => document.querySelector("#threadlisttableid")), (() => { this.checkDom(); })), this.handleImg()); } checkDom() { const e = document.querySelector("#threadlisttableid"); if (!e) return void console.error("没有找到容器节点", e); const t = new MutationObserver((async n => { t.disconnect(); try { this.parseArticleImg().then(); } finally { t.observe(e, r); } })), r = { childList: !0, subtree: !1 }; t.observe(e, r); } async parseArticleImg() { let e = {}; const t = localStorage.getItem("articleImagesCache"); t && (e = JSON.parse(t)), $(".s.xst").each((async (t, r) => { const n = $(r).attr("href"); if (e[n]) { const t = $(r).closest("tbody"); return t.find(".imageBox").length || t.append(e[n]), void this.processedArticles.add(n); } if (!this.processedArticles.has(n)) { this.processedArticles.add(n); try { const t = $(r).closest("tbody"); if (t.find(".imageBox").length) return; if (!t.is(":visible")) return; const s = await fetch(n); if (!s.ok) return; const o = $($.parseHTML(await s.text())).find("img.zoom[file]:not([file*='static'], [file*='hrline'])").slice(0, 5); if (!o.length) return; const i = `\n <tr class="imageBox">\n <td colspan="5">\n <div style="display:flex;gap:10px;overflow-x:auto;padding:5px 0">${o.map(((e, t) => `<img src="${$(t).attr("file")}" style="width:300px;height:auto;max-width:300px;max-height:300px;object-fit:contain" onclick="zoom(this,this.src,0,0,0)" alt="">`)).get().join("")}</div>\n </td>\n </tr>\n `; e[n] = i, localStorage.setItem("articleImagesCache", JSON.stringify(e)), t.append(i); } catch (s) { console.error("Error:", n, s); } } })); } handleImg() { document.addEventListener("click", (e => { if ("IMG" === e.target.tagName && e.target.closest(".imageBox")) { const t = e.target.closest(".imageBox"); this.currentImageGroup = Array.from(t.querySelectorAll("img")), this.currentImageIndex = this.currentImageGroup.indexOf(e.target), this.createNavigateBtn(); } })); } createNavigateBtn() { utils.loopDetector((() => $("#imgzoom_picpage").length > 0), (() => { if (0 === $("#imgzoom_picpage").length) return; const e = document.getElementById("imgzoom_picpage"); if (!e) return; e.querySelectorAll("#zimg_prev, #zimg_next").forEach((e => e.remove())); const t = document.createElement("div"); t.id = "zimg_prev", t.className = "zimg_prev", t.onclick = () => this.navigateImage(-1); const r = document.createElement("div"); r.id = "zimg_next", r.className = "zimg_next", r.onclick = () => this.navigateImage(1), e.append(t, r); })); } navigateImage(e) { this.currentImageIndex = (this.currentImageIndex + e + this.currentImageGroup.length) % this.currentImageGroup.length; const t = this.currentImageGroup[this.currentImageIndex]; zoom(t, t.src, 0, 0, 0), this.createNavigateBtn(); } }), s.process().then();