Sleazy Fork is available in English.

FORUM-JHS

色花堂支持帖外预览图片

// ==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();