JAV-FORUM

Alt+左|右键 激活菜单; 支持以图识图、磁力聚合...

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         JAV-FORUM
// @description  Alt+左|右键 激活菜单; 支持以图识图、磁力聚合...
// @version      0.0.5
// @author       JAV-FORUM
// @namespace    JAV-FORUM
// @license      MIT
// @icon         https://cdn-icons-png.flaticon.com/512/6576/6576105.png
// @include      *://*/*
// @exclude      *://*sleazyfork.org/*
// @exclude      *://*greasyfork.org/*
// @exclude      *://*gemini.google.com/*
// @exclude      *://*googletagmanager.com/*
// @exclude      *://*ogs.google.com/*
// @exclude      *://*javdb*.com/*
// @require      https://update.greasyfork.org/scripts/515994/1478507/gh_2215_make_GM_xhr_more_parallel_again.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/layer.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/viewer.min.js
// @connect      whatslink.info
// @connect      u9a9.com
// @connect      sukebei.nyaa.si
// @connect      btsow.lol
// @connect      btdig.com
// @connect      cld139.buzz
// @connect      bt4gprx.com
// @connect      api.imgur.com
// @connect      115.com
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM_openInTab
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// ==/UserScript==

var __defProp = Object.defineProperty, __typeError = msg => {
    throw TypeError(msg);
}, __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
    enumerable: !0,
    configurable: !0,
    writable: !0,
    value: value
}) : obj[key] = value, __publicField = (obj, key, value) => __defNormalProp(obj, "symbol" != typeof key ? key + "" : key, value), __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg), __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), 
getter ? getter.call(obj) : member.get(obj)), __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);

!function() {
    "use strict";
    var _timers, _intervalContainer;
    class NetUtil {
        constructor() {
            throw new Error("工具类不可实例化");
        }
        static getBaseUrl(url) {
            return new URL(url).origin;
        }
        static async retry(fun, tryCount = 3) {
            let runCount = 0;
            for (;runCount < tryCount; ) try {
                const result = await fun();
                runCount > 0 && console.debug(`[重试] 成功,共发起 ${runCount + 1} 次。`);
                return result;
            } catch (e2) {
                let errorString = String(e2);
                errorString.startsWith("Error: ") && (errorString = errorString.replace("Error: ", ""));
                if (errorString.includes("Just a moment") || errorString.includes("重定向") || errorString.toLowerCase().includes("404 page not found") || errorString.toLowerCase().includes("沒有您要的結果") || errorString.toLowerCase().includes("状态码:4") || errorString.toLowerCase().includes("404 not found")) throw e2;
                runCount++;
                if (runCount === tryCount) {
                    errorString.length > 200 ? console.debug(`[重试] 达到最大重试次数 (${tryCount}),最终失败`) : console.debug(`[重试] 达到最大重试次数 (${tryCount}),最终失败:`, e2);
                    throw e2;
                }
                errorString.length > 200 ? console.debug(`[重试] 准备第 ${runCount + 1} 次重试`) : console.debug(`[重试] 准备第 ${runCount + 1} 次重试, 错误信息: ${errorString}`);
            }
        }
    }
    window.cacheManager = new class {
        constructor() {
            __publicField(this, "image_recognition_site_key", "jhs_image_recognition_site");
            __publicField(this, "image_recognition_history_key", "jhs_image_recognition_history");
            __publicField(this, "image_recognition_auto_open_key", "jhs_image_recognition_auto_open");
            __publicField(this, "magnetHubSortType_key", "jhs_magnetHubSortType");
            __publicField(this, "magnetHubEngines_key", "jhs_magnetHubEngines");
            __publicField(this, "magnetHubHistory_key", "jhs_magnetHistory");
            __publicField(this, "magnetExtractorCollapsed_key", "jhs_magnetExtractorCollapsed");
            __publicField(this, "menuSetting_key", "jhs_menuSetting");
        }
        setItem(key, value) {
            try {
                GM_setValue(key, value);
            } catch (error) {
                console.error(`【缓存失败】写入键名 "${key}" 时出错:`, error);
            }
        }
        getItem(key, defaultValue = null) {
            return GM_getValue(key, defaultValue);
        }
        removeItem(key) {
            GM_deleteValue(key);
        }
    };
    window.tempCacheManager = new class {
        setItem(key, value) {
            try {
                const stringValue = "string" == typeof value ? value : JSON.stringify(value);
                sessionStorage.setItem(key, stringValue);
            } catch (error) {
                console.error("【Session写入失败】数据过大或存储受限", error);
            }
        }
        getItem(key, defaultValue = null) {
            const rawData = sessionStorage.getItem(key);
            if (null === rawData) return defaultValue;
            try {
                return JSON.parse(rawData);
            } catch (e2) {
                return rawData;
            }
        }
        removeItem(key) {
            sessionStorage.removeItem(key);
        }
        removeAll() {
            sessionStorage.clear();
        }
    };
    window.gmHttp = new class {
        async get(url, headers = {}, options = {}) {
            options.headers = headers;
            return this.gmRequest("GET", url, null, options);
        }
        postJson(url, data = {}, headers = {}, options = {}) {
            let jsonData = JSON.stringify(data);
            options.headers = {
                "Content-Type": "application/json",
                ...options.headers
            };
            return this.gmRequest("POST", url, jsonData, options);
        }
        postFormData(url, formData, headers = {}, options = {
            timeout: 1e4,
            retryCount: 2
        }) {
            if (!(formData instanceof FormData)) throw new Error("参数类型错误 需为 FormData 实例");
            options.headers = headers;
            return this.gmRequest("POST", url, formData, options);
        }
        async gmRequest(method, url, data, options = {}) {
            const {headers: headers = {}, noRedirect: noRedirect = !1, timeout: timeout = null, retryCount: retryCount = null} = options, httpTimeout = timeout || 2e3, httpRetryCount = retryCount || 5;
            data || (data = void 0);
            return await NetUtil.retry((() => new Promise(((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: method,
                    url: url,
                    headers: headers,
                    timeout: httpTimeout,
                    data: data,
                    onload: response => {
                        try {
                            noRedirect && response.finalUrl !== url && reject(`请求被重定向了, URL是: ${response.finalUrl} 内容:${response.responseText}`);
                            if (response.status >= 200 && response.status < 300) if (response.responseText) try {
                                resolve(JSON.parse(response.responseText));
                            } catch (e2) {
                                resolve(response.responseText);
                            } else resolve(response.responseText || response); else {
                                console.error("请求失败,状态码:", response.status, url);
                                if (response.responseText) try {
                                    const errorData = JSON.parse(response.responseText);
                                    reject(errorData);
                                } catch {
                                    let rawText = response.responseText, errorMessage = "";
                                    if (rawText.includes("<html") || rawText.includes("<!DOCTYPE")) {
                                        const titleMatch = rawText.match(/<title>(.*?)<\/title>/i);
                                        errorMessage = titleMatch && titleMatch[1] ? `HTML Error: ${titleMatch[1].trim()}` : rawText.replace(/<[^>]+>/g, " ").slice(0, 100).trim() + "...";
                                    } else errorMessage = rawText.length > 200 ? rawText.slice(0, 200) + "..." : rawText;
                                    const finalMsg = `${errorMessage} (状态码:${response.status})`;
                                    reject(new Error(finalMsg || `请求发生错误 ${response.status}`));
                                } else reject(new Error(`请求发生错误 ${response.status}`));
                            }
                        } catch (e2) {
                            reject(e2);
                        }
                    },
                    onerror: error => {
                        console.error("网络错误:", url);
                        reject(new Error(error.error || "网络错误"));
                    },
                    ontimeout: () => {
                        reject(new Error("请求超时: " + url));
                    }
                });
            }))), httpRetryCount);
        }
    };
    const _DomUtil = class {
        constructor() {
            throw new Error("工具类不可实例化");
        }
        static importResource(url) {
            let tag;
            if (url.indexOf("css") >= 0) {
                tag = document.createElement("link");
                tag.setAttribute("rel", "stylesheet");
                tag.href = url;
            } else {
                tag = document.createElement("script");
                tag.setAttribute("type", "text/javascript");
                tag.src = url;
            }
            document.documentElement.appendChild(tag);
        }
        static htmlTo$dom(html, filterAvatarBox = !0) {
            const parser = new DOMParser;
            return $(parser.parseFromString(html, "text/html"));
        }
    };
    __publicField(_DomUtil, "insertStyle", ((css, id) => {
        if (!css) return;
        if (id && $(`#${id}`).length > 0) {
            console.warn(`[insertStyle] 插入失败:ID 为 "${id}" 的样式表已存在。`);
            return;
        }
        const finalCss = `<style${id ? ` id="${id}"` : ""}>${css.replace(/<\/?style>/gi, "")}</style>`;
        $("head").append(finalCss);
    }));
    __publicField(_DomUtil, "debounce", ((func, delay) => {
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout((() => func.apply(_DomUtil, args)), delay);
        };
    }));
    let DomUtil = _DomUtil;
    const btnCss = `\n<style>\n    jhs-btn {\n        min-width: 80px;\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        padding: 7px 15px;\n        margin-right: 5px;\n        border-radius: 7px;\n        text-decoration: none;\n        font-weight: bold;\n        font-size: 12px;\n        transition: all 0.1s ease-in;\n        cursor: pointer;\n        white-space: nowrap;\n        box-sizing: border-box;\n        color: white; /* 默认字体颜色 */\n        box-shadow: 0 2px 3px rgba(0, 0, 0, 0.15);\n    }\n\n    jhs-btn:hover {\n        transform: translateY(-1px);\n        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15);\n    }\n    \n    /* --- 内部 a 标签处理 --- */\n    jhs-btn a,\n    jhs-btn a:visited{\n        text-decoration: none; \n        color: inherit !important; \n    }\n    jhs-btn a:hover {\n        color: white;\n    }\n    \n    /* --- 动态生成的配色 --- */\n    ${Object.entries({
        aliceBlue: {
            background: "#f0f9ff",
            color: "#0369a1"
        },
        blue: {
            background: "#409EFF"
        },
        royalBlue: {
            background: "#2563eb"
        },
        denimBlue: {
            background: "#1d4ed8"
        },
        indigo: {
            background: "#3F51B5"
        },
        yellow: {
            background: "#E6A23C"
        },
        orange: {
            background: "#FF9800"
        },
        red: {
            background: "#d22020"
        },
        teal: {
            background: "#009688"
        },
        green: {
            background: "#67C23A"
        },
        brown: {
            background: "#795548"
        },
        white: {
            background: "#fff",
            color: "#424242"
        },
        whitesmoke: {
            background: "#F5F5F5",
            color: "#424242"
        },
        black: {
            background: "#212121"
        },
        pink: {
            background: "#E91E63"
        }
    }).map((([type, props]) => `\n    jhs-btn[type="${type}"] {\n        ${Object.entries(props).map((([key, value]) => `${key}: ${value};`)).join("\n\t\t")}\n    }`)).join("\n")}\n\n    /* --- 按钮尺寸 --- */\n    jhs-btn[size="mini"] {\n        padding: 5px 10px;\n        font-size: 10px;\n    }\n    \n    jhs-btn[size="small"] {\n        padding: 7px 10px;\n        font-size: 11px;\n    }\n    \n    jhs-btn[size="large"] {\n        padding: 10px 20px;\n        font-size: 14px;\n    }\n    \n</style>\n`;
    DomUtil.insertStyle(btnCss);
    !function() {
        const colors_infoStart = "#60A5FA", colors_infoEnd = "#93C5FD", colors_successStart = "#10B981", colors_successEnd = "#6EE7B7", colors_errorStart = "#EF4444", colors_errorEnd = "#FCA5A5", commonStyles = {
            borderRadius: "12px",
            color: "white",
            padding: "10px 16px",
            boxShadow: "0 4px 6px rgba(0,0,0,0.1)",
            minWidth: "150px",
            textAlign: "center",
            fontSize: "15px",
            zIndex: 99999999999
        }, typeStyles = {
            info: {
                ...commonStyles,
                background: `linear-gradient(to right, ${colors_infoStart}, ${colors_infoEnd})`
            },
            success: {
                ...commonStyles,
                background: `linear-gradient(to right, ${colors_successStart}, ${colors_successEnd})`
            },
            error: {
                ...commonStyles,
                background: `linear-gradient(to right, ${colors_errorStart}, ${colors_errorEnd})`
            }
        }, createToastInterface = (configOverrides = {}) => {
            const runShow = (type, ...msgParts) => {
                let msg = msgParts.map((part => {
                    if (part instanceof Error) return part.message;
                    if ("object" == typeof part && null !== part) try {
                        return JSON.stringify(part);
                    } catch (e2) {}
                    return String(part);
                })).join(" ");
                msg.length > 500 && (msg = msg.substring(0, 500) + "... (内容超长已省略)");
                return ((msg, type, finalOptions) => {
                    "center" === finalOptions.gravity && (finalOptions.offset = {
                        y: "calc(50vh - 150px)"
                    });
                    let callback = finalOptions.callback || finalOptions.onClose || finalOptions.end;
                    const defaultConfig = {
                        text: msg,
                        escapeMarkup: !1,
                        duration: "error" === type ? 2500 : 2e3,
                        close: !1,
                        gravity: "bottom",
                        position: "right",
                        style: typeStyles[type],
                        stopOnFocus: !0,
                        oldestFirst: !1,
                        callback: callback,
                        ...finalOptions
                    };
                    -1 === defaultConfig.duration && (defaultConfig.close = !0);
                    const toast = Toastify(defaultConfig);
                    toast.showToast();
                    toast.closeShow = () => {
                        toast.toastElement.remove();
                    };
                    return toast;
                })(msg, type, configOverrides);
            };
            return {
                ok: (...msg) => runShow("success", ...msg),
                error: (...msg) => runShow("error", ...msg),
                info: (...msg) => runShow("info", ...msg)
            };
        }, BaseShow = createToastInterface({});
        BaseShow.config = options => createToastInterface(options);
        window.show = BaseShow;
    }();
    !function() {
        document.head.insertAdjacentHTML("beforeend", '\n        <style>\n            /* * loading-container-fixed: 用于 body,确保始终在视口中心\n             * loading-container-absolute: 用于特定元素,确保覆盖该元素\n             */\n            .loading-container-base {\n                top: 0;\n                left: 0;\n                width: 100%;\n                height: 100%;\n                display: flex;\n                flex-direction: column;\n                justify-content: center;\n                align-items: center;\n                background-color: rgba(0, 0, 0, 0.1);\n                z-index: 99999999;\n                box-sizing: border-box; \n                /* 确保内容不会溢出,如果需要更复杂的布局,可能需要调整 */\n                overflow: hidden; \n            }\n\n            .loading-container-absolute {\n                position: absolute; /* 相对于父元素定位 */\n            }\n            \n            .loading-container-fixed {\n                position: fixed; /* 相对于视口定位 */\n            }\n            \n            .loading-animation {\n                position: relative;\n                width: 60px;\n                height: 12px;\n                background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);\n                border-radius: 6px;\n                animation: loading-animate 1.8s ease-in-out infinite;\n                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n                margin-bottom: 30px; /* 增加底部间距以容纳自定义内容 */\n            }\n    \n            .loading-animation:before,\n            .loading-animation:after {\n                position: absolute;\n                display: block;\n                content: "";\n                animation: loading-animate 1.8s ease-in-out infinite;\n                height: 12px;\n                border-radius: 6px;\n                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n            }\n    \n            .loading-animation:before {\n                top: -20px;\n                left: 10px;\n                width: 40px;\n                background: linear-gradient(90deg, #ff758c 0%, #ff7eb3 100%);\n            }\n    \n            .loading-animation:after {\n                bottom: -20px;\n                width: 35px;\n                background: linear-gradient(90deg, #ff9a9e 0%, #fad0c4 100%);\n            }\n            \n            @keyframes loading-animate {\n                0% {\n                    transform: translateX(40px);\n                }\n                50% {\n                    transform: translateX(-30px);\n                }\n                100% {\n                    transform: translateX(40px);\n                }\n            }\n\n            /* 新增:自定义内容样式 */\n            .loading-custom-content {\n                color: #333; /* 默认颜色 */\n                padding: 10px 20px;\n                background-color: rgba(255, 255, 255, 0.9);\n                border-radius: 5px;\n                box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n                max-width: 80%;\n                text-align: center;\n            }\n        </style>\n    ');
        window.loading = function(targetElement, options = {}) {
            let parentElement, isBody = !1;
            if ("string" == typeof targetElement && targetElement) parentElement = document.querySelector(targetElement); else if (targetElement instanceof HTMLElement) parentElement = targetElement; else if (targetElement && "object" == typeof targetElement && !targetElement.nodeType) {
                options = targetElement;
                parentElement = document.body;
            } else parentElement = document.body;
            "object" == typeof options && null !== options || (options = {});
            if (!parentElement) {
                console.error("Loading 目标元素未找到!");
                return null;
            }
            isBody = parentElement === document.body;
            const container = document.createElement("div");
            container.classList.add("loading-container-base");
            if (isBody) container.classList.add("loading-container-fixed"); else {
                container.classList.add("loading-container-absolute");
                "static" === window.getComputedStyle(parentElement).position && (parentElement.style.position = "relative");
            }
            const animation = document.createElement("div");
            animation.className = "loading-animation";
            const {contentHTML: contentHTML} = options;
            let contentElement = null;
            if (contentHTML) {
                contentElement = document.createElement("div");
                contentElement.className = "loading-custom-content";
                contentElement.innerHTML = contentHTML;
            }
            container.appendChild(animation);
            contentElement && container.appendChild(contentElement);
            parentElement.appendChild(container);
            return {
                close: () => {
                    container && container.parentNode && container.parentNode.removeChild(container);
                }
            };
        };
        window.isLoading = () => $(".loading-container-base").length > 0;
    }();
    class ViewerManager {
        constructor() {
            this.viewerInstance = null;
            this.$container = null;
        }
        _initStyles() {
            if (document.getElementById("viewer-manager-style")) return;
            const style = document.createElement("style");
            style.id = "viewer-manager-style";
            style.innerHTML = "\n            /* 1. 强制显示滚动条并增加容器布局 */\n            .viewer-canvas { \n                overflow: auto !important; \n                scrollbar-gutter: stable; /* 防止滚动条出现时画面闪烁 */\n            }\n    \n            /* 2. 定义滚动条整体宽度 */\n            .viewer-canvas::-webkit-scrollbar {\n                width: 18px !important;  /* 纵向滚动条宽度 */\n                height: 12px !important; /* 横向滚动条高度 */\n                display: block !important;\n            }\n    \n            /* 3. 滚动条轨道 (背景) */\n            .viewer-canvas::-webkit-scrollbar-track {\n                background: rgba(0, 0, 0, 0.1) !important;\n                border-radius: 10px;\n            }\n    \n            /* 4. 滚动条滑块 (滑块本身) */\n            .viewer-canvas::-webkit-scrollbar-thumb {\n                background: rgba(255, 255, 255, 0.3) !important; /* 半透明白色 */\n                border-radius: 10px;\n                border: 2px solid rgba(0, 0, 0, 0.2); /* 给滑块加个边框 */\n            }\n    \n            /* 5. 滑块悬停效果 */\n            .viewer-canvas::-webkit-scrollbar-thumb:hover {\n                background: rgba(255, 255, 255, 0.5) !important;\n                border: 1px solid rgba(255, 255, 255, 0.8);\n            }\n        \n            .viewer-canvas { overflow: auto !important; }\n            .viewer-close { background: rgba(255,0,0,0.6) !important; }\n            .viewer-close:hover { background: rgba(255,0,0,0.8) !important; }\n            #global-viewer-container { display: none; }\n            \n            /* 单图模式 UI 隐藏逻辑 */\n            .viewer-is-single .viewer-navbar,\n            .viewer-is-single .viewer-prev,\n            .viewer-is-single .viewer-next {\n                display: none !important;\n            }\n        ";
            document.head.appendChild(style);
        }
        _initContainer() {
            this.$container = $("#global-viewer-container");
            0 === this.$container.length && (this.$container = $('<div id="global-viewer-container"></div>').appendTo("body"));
        }
        _createInstance() {
            if (this.viewerInstance) return;
            this._initStyles();
            this._initContainer();
            this._boundWheelHandler = this._handleWheel.bind(this);
            const options = {
                zIndex: 999999990,
                navbar: !0,
                zoomOnWheel: !1,
                zoomRatio: .1,
                toggleOnDblclick: !1,
                transition: !0,
                toolbar: {
                    zoomIn: 1,
                    zoomOut: 1,
                    oneToOne: 1,
                    prev: 1,
                    next: 1
                },
                render: function(e2) {
                    const $scrollBtn = $(e2.currentTarget).find(".viewer-scrollDown");
                    $scrollBtn.html("↓");
                    $scrollBtn.attr("title", "向下滚动");
                },
                title: !1,
                keyboard: !1,
                show: () => {},
                shown: () => {
                    this._toggleScroll(!0);
                    document.addEventListener("keydown", this._handleKeydown.bind(this));
                    this.viewerInstance.canvas.addEventListener("wheel", this._boundWheelHandler, {
                        passive: !1
                    });
                },
                view() {},
                viewed: () => {
                    const instance = this.viewerInstance, imgSrc = instance.image.src;
                    if (this.currentConfig.initZoom) {
                        const targetRatio = imgSrc.includes("javfree") ? 1 : 1.4;
                        instance.zoomTo(targetRatio);
                    }
                    if (this.currentConfig.toTop) {
                        const left = (instance.viewerData.width - instance.imageData.width) / 2;
                        instance.moveTo(left, 0);
                    }
                },
                zoom: () => {},
                zoomed: () => {},
                move: () => {},
                moved: () => {},
                hide: () => {
                    document.activeElement && document.activeElement.blur();
                    document.body.focus();
                },
                hidden: () => {
                    this._toggleScroll(!1);
                    document.removeEventListener("keydown", this._handleKeydown.bind(this));
                    this.viewerInstance && this.viewerInstance.canvas && this.viewerInstance.canvas.removeEventListener("wheel", this._boundWheelHandler);
                    this._smartToggleBodyScroll();
                }
            };
            this.viewerInstance = new Viewer(this.$container[0], options);
        }
        _handleWheel(e2) {
            if (!e2.ctrlKey) return;
            e2.preventDefault();
            const now = Date.now();
            if (this._lastZoomTime && now - this._lastZoomTime < 50) return;
            this._lastZoomTime = now;
            const ratio = e2.deltaY < 0 ? .1 : -.1;
            requestAnimationFrame((() => {
                this.viewerInstance.zoom(ratio, !0, {
                    x: e2.pageX,
                    y: e2.pageY
                });
            }));
        }
        _toggleScroll(lock) {
            const action = lock ? "hidden" : "";
            document.documentElement.style.overflow = action;
            document.body.style.overflow = action;
        }
        _smartToggleBodyScroll(waitTime = 10) {
            setTimeout((() => {
                const hasShade = document.querySelectorAll(".layui-layer-shade").length > 0;
                document.documentElement.style.overflow = hasShade ? "hidden" : "";
            }), waitTime);
        }
        _handleKeydown(e2) {
            if ("Escape" === e2.key || " " === e2.key) {
                e2.preventDefault();
                this.close();
            }
        }
        showImageViewer(urls, config = {}) {
            if (!Array.isArray(urls)) {
                console.error("[ViewerLog] ❌ urls 必须是数组");
                return;
            }
            this.currentConfig = Object.assign({
                startIndex: 0,
                toTop: !0,
                initZoom: !0
            }, config);
            this._createInstance();
            const imgHtml = urls.map((url => `<img src="${url}" referrerPolicy="no-referrer" alt="">`)).join("");
            this.$container.html(imgHtml);
            this.viewerInstance.update();
            const isSingle = urls.length <= 1;
            this.viewerInstance.viewer.classList.toggle("viewer-is-single", isSingle);
            this.viewerInstance.view(this.currentConfig.startIndex);
            this.viewerInstance.show();
        }
        close() {
            this.viewerInstance && this.viewerInstance.hide();
        }
        isShowing() {
            return !(!this.viewerInstance || !this.viewerInstance.isShown);
        }
    }
    !async function() {
        window.viewerManager = new ViewerManager;
    }();
    class PluginManager {
        constructor() {
            this.plugins = new Map;
        }
        register(pluginClass) {
            if ("function" != typeof pluginClass) throw new Error("插件必须是一个类");
            const instance = new pluginClass;
            instance.pluginManager = this;
            if (!instance.getRegisterCondition()) return;
            const lowerName = instance.getName().toLowerCase();
            if (this.plugins.has(lowerName)) throw new Error(`插件"${name}"已注册`);
            this.plugins.set(lowerName, instance);
        }
        getBean(name2) {
            return this.plugins.get(name2.toLowerCase());
        }
        async processCss() {
            const failedCssLoads = (await Promise.allSettled(Array.from(this.plugins).map((async ([name2, instance]) => {
                try {
                    if ("function" == typeof instance.initCss) {
                        const css = await instance.initCss();
                        css && DomUtil.insertStyle(css, name2);
                        return {
                            name: name2,
                            status: "fulfilled"
                        };
                    }
                    return {
                        name: name2,
                        status: "skipped"
                    };
                } catch (e2) {
                    console.error(`插件 ${name2} 加载 CSS 失败`, e2);
                    return {
                        name: name2,
                        status: "rejected",
                        error: e2
                    };
                }
            })))).filter((r => "rejected" === r.status));
            failedCssLoads.length && console.error("以下插件的 CSS 加载失败:", failedCssLoads.map((p => p.value.name)));
        }
        async processPlugins() {
            await Promise.all(Array.from(this.plugins).map((async ([name2, instance]) => {
                try {
                    "function" == typeof instance.handle && await instance.handle();
                } catch (e2) {
                    console.error(`插件 ${name2} 执行失败`, e2);
                }
            })));
        }
    }
    class BasePlugin {
        constructor() {
            __publicField(this, "pluginManager", null);
        }
        getName() {
            throw new Error(`${this.constructor.name} 未显示getName()`);
        }
        getBean(name2) {
            let bean = this.pluginManager.getBean(name2);
            if (!bean) {
                let msg = "容器中不存在: " + name2;
                show.error(msg);
                throw new Error(msg);
            }
            return bean;
        }
        getRegisterCondition() {
            throw new Error(`${this.constructor.name} 未返回注册条件getRegisterCondition()`);
        }
        async initCss() {
            return "";
        }
        async handle() {}
    }
    class DateUtil {
        constructor() {
            throw new Error("工具类不可实例化");
        }
        static formatDate(date, dateSplitStr = "-", timeSplitStr = ":") {
            let targetDate;
            if (date instanceof Date) targetDate = date; else {
                if ("string" != typeof date) throw new Error("Invalid date input: must be Date object or date string");
                targetDate = new Date(date);
                if (isNaN(targetDate.getTime())) throw new Error("Invalid date string");
            }
            const year = targetDate.getFullYear(), month = String(targetDate.getMonth() + 1).padStart(2, "0"), day = String(targetDate.getDate()).padStart(2, "0"), hours = String(targetDate.getHours()).padStart(2, "0"), minutes = String(targetDate.getMinutes()).padStart(2, "0"), seconds = String(targetDate.getSeconds()).padStart(2, "0");
            return `${[ year, month, day ].join(dateSplitStr)} ${[ hours, minutes, seconds ].join(timeSplitStr)}`;
        }
        static toTimestamp(input = new Date, isSecond = !1) {
            let date;
            if (input instanceof Date) date = input; else {
                if ("string" != typeof input) throw new Error("参数类型错误:仅支持 Date 对象或日期字符串");
                date = new Date(input);
            }
            const timestamp = date.getTime();
            if (isNaN(timestamp)) throw new Error(`无效的日期格式: ${input}`);
            return isSecond ? Math.floor(timestamp / 1e3) : timestamp;
        }
    }
    _timers = new WeakMap;
    __privateAdd(DateUtil, _timers, new Map);
    __publicField(DateUtil, "SECOND", 1e3);
    __publicField(DateUtil, "MINUTE", 6e4);
    __publicField(DateUtil, "HOUR", 36e5);
    __publicField(DateUtil, "DAY", 864e5);
    __publicField(DateUtil, "WEEK", 6048e5);
    __publicField(DateUtil, "MONTH", 2592e6);
    const _CommonUtil = class _CommonUtil {
        constructor() {
            throw new Error("工具类不可实例化");
        }
        static loopDetector(condition, after, detectInterval = 20, timeout = 1e4, runWhenTimeout = !1) {
            const uuid = Math.random(), start = (new Date).getTime(), stopAndRun = shouldRun => {
                clearInterval(__privateGet(_CommonUtil, _intervalContainer)[uuid]);
                shouldRun && after && after();
                delete __privateGet(_CommonUtil, _intervalContainer)[uuid];
            };
            __privateGet(_CommonUtil, _intervalContainer)[uuid] = setInterval((() => {
                const timeElapsed = (new Date).getTime() - start;
                condition() ? stopAndRun(!0) : timeElapsed >= timeout && stopAndRun(runWhenTimeout);
            }), detectInterval);
        }
        static copyToClipboard(text, thenFun, errorFun) {
            navigator.clipboard.writeText(text).then((() => {
                thenFun && thenFun();
            })).catch((err => {
                show.error("复制失败: ", err);
                errorFun && errorFun();
            }));
        }
        static isNull(value) {
            return null == value || ("string" == typeof value ? "" === value.trim() : Array.isArray(value) ? 0 === value.length : "object" == typeof value && 0 === Object.keys(value).length);
        }
        static isNotNull(value) {
            return !this.isNull(value);
        }
        static q(event, msg, fun, cancelFun, shade, area) {
            let offset;
            event && (offset = [ event.clientY - 120, event.clientX - 120 ]);
            shade || (shade = 0);
            let confirmIndex = layer.confirm(msg, {
                offset: offset,
                title: "提示",
                btn: [ "确定", "取消" ],
                shade: shade,
                area: area,
                zIndex: 9999999999999
            }, (function() {
                fun && fun();
                layer.close(confirmIndex);
            }), (function() {
                cancelFun && cancelFun();
            }));
            return confirmIndex;
        }
    };
    _intervalContainer = new WeakMap;
    __privateAdd(_CommonUtil, _intervalContainer, {});
    let CommonUtil = _CommonUtil;
    class MagnetHubPlugin extends BasePlugin {
        constructor() {
            super(...arguments);
            __publicField(this, "engineConfig", [ {
                id: "cldcld",
                label: "磁力帝",
                targetPage: "https://www.cld139.buzz/search-{keyword}-0-0-1.html",
                url: "https://www.cld139.buzz/search-{keyword}-0-0-1.html",
                handler: (url, keyword) => this.parseCld(url, keyword)
            }, {
                id: "btdig",
                label: "BTDIG",
                targetPage: "https://btdig.com/search?q={keyword}",
                url: "https://btdig.com/search?q={keyword}",
                handler: (url, keyword) => this.parseBtdig(url, keyword)
            }, {
                id: "bt4g",
                label: "BT4G",
                targetPage: "https://bt4gprx.com/search?q={keyword}&orderby=relevance",
                url: "https://bt4gprx.com/search?q={keyword}&orderby=relevance",
                handler: (url, keyword) => this.parseBt4g(url, keyword)
            }, {
                id: "u9a9",
                label: "U9A9",
                targetPage: "https://u9a9.com/?type=2&search={keyword}",
                url: "https://u9a9.com/?type=2&search={keyword}",
                handler: (url, keyword) => this.commonParse(url, keyword)
            }, {
                id: "sukebei",
                label: "Sukebei",
                targetPage: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}",
                url: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}",
                handler: (url, keyword) => this.commonParse(url, keyword)
            }, {
                id: "btsow",
                label: "BTSOW",
                targetPage: "https://btsow.lol/search/{keyword}",
                url: "https://btsow.lol/bts/data/api/search",
                handler: (url, keyword) => this.parseBTSOW(url, keyword)
            } ]);
            __publicField(this, "currentSort", "");
            __publicField(this, "currentSearchTicket", 0);
            __publicField(this, "highlightKeywords", [ "-c", "破解", "流出", "-AI", "无码", "4k", "8k", "-uc", "-u" ]);
            __publicField(this, "maxHistoryCount", 20);
        }
        getName() {
            return "MagnetHubPlugin";
        }
        getRegisterCondition() {
            return !0;
        }
        async initCss() {
            return '\n            <style>\n                .magnet-container {\n                    margin: 0 auto;\n                    padding: 10px 20px;\n                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\n                    width: 100%;             \n                    box-sizing: border-box;\n                }\n    \n                .magnet-engine-selector {\n                    display: flex;\n                    justify-content: space-between;\n                    align-items: center;\n                    background: #f8fafc;\n                    padding: 10px 15px;\n                    border-radius: 8px;\n                    margin-bottom: 15px;\n                    border: 1px solid #e2e8f0;\n                }\n                \n                .engine-group {\n                    display: flex;\n                    flex-wrap: wrap;\n                    gap: 15px;\n                    flex: 1;\n                }\n                \n                .engine-actions {\n                    display: flex;\n                    align-items: center;\n                    gap: 15px;\n                    padding-left: 20px;\n                }\n                \n                .magnet-results {\n                    min-height: 150px;\n                }\n                .magnet-result {\n                    background: #fff;\n                    border-radius: 10px;\n                    padding: 16px;\n                    margin-bottom: 12px;\n                    border: 1px solid #e2e8f0;\n                    transition: transform 0.2s, box-shadow 0.2s;\n                    display: flex;\n                    justify-content: space-between;\n                    flex-direction: column; \n                    border-left-color: #2563eb;\n                }\n                .magnet-result:hover {\n                    transform: translateY(-2px);\n                    box-shadow: 0 4px 12px rgba(0,0,0,0.08);\n                }\n                \n                .magnet-info {\n                    display: flex; \n                    justify-content: space-between; \n                    align-items: flex-start;\n                    flex-wrap: wrap;\n                }\n    \n                .magnet-content {\n                    flex: 1;\n                    min-width: 0;\n                }\n                .magnet-title {\n                    font-size: 14px;\n                    font-weight: 500;\n                    color: #334155;\n                    margin-bottom: 6px;\n                    text-decoration: none !important;\n                    overflow: hidden;\n                    text-overflow: ellipsis;\n                    line-height: 1.4;\n                    word-wrap: break-word; \n                    overflow-wrap: break-word;\n                    white-space: normal;\n                }\n                .magnet-title:hover {\n                    color: #2563eb;\n                    text-decoration: underline !important;\n                }\n                .magnet-meta {\n                    display: flex;\n                    gap: 12px;\n                    font-size: 12px;\n                    color: #94a3b8;\n                    font-weight: 400;\n                    margin-top: 10px;\n                    flex-wrap: wrap;\n                }\n                .magnet-meta a:hover {\n                    text-decoration: underline !important;\n                    opacity: 0.8;\n                }\n\n    \n                .magnet-actions {\n                    display: flex;\n                    gap: 2px;\n                    flex-shrink: 0;\n                }\n                \n                .magnet-loading {\n                    display: flex;\n                    flex-direction: column;\n                    align-items: center;\n                    justify-content: center;\n                    padding: 40px 0;\n                    color: #94a3b8;\n                    font-size: 14px;\n                }\n\n                .magnet-error {\n                    text-align: center;\n                    padding: 20px;\n                    color: #ef4444;\n                    background: #fef2f2;\n                    border-radius: 8px;\n                    font-size: 13px;\n                    margin: 10px 0;\n                }\n    \n                .magnet-tools {\n                    display: flex;\n                    gap: 15px;\n                    padding: 0 5px 10px;\n                    font-size: 12px;\n                    color: #64748b;\n                    align-items: center;\n                }\n                .sort-item {\n                    cursor: pointer;\n                    transition: color 0.2s;\n                }\n                .sort-item.active {\n                    color: #2563eb;\n                    font-weight: bold;\n                }\n                .sort-item:hover {\n                    color: #2563eb;\n                }\n\n                .magnet-search-bar {\n                    display: flex;\n                    gap: 8px;\n                    margin-bottom: 15px;\n                    padding: 0 5px;\n                }\n                .magnet-input {\n                    flex: 1;\n                    padding: 8px 12px;\n                    border: 1px solid #e2e8f0;\n                    border-radius: 6px;\n                    font-size: 13px;\n                    outline: none;\n                    transition: border-color 0.2s;\n                }\n                .magnet-input:focus {\n                    border-color: #2563eb;\n                    box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);\n                }\n                .search-submit-btn {\n                    background-color: #2563eb;\n                    color: white;\n                    border: none;\n                    padding: 0 16px;\n                    border-radius: 6px;\n                    cursor: pointer;\n                    font-size: 13px;\n                    font-weight: 500;\n                }\n                .search-submit-btn:hover {\n                    background-color: #1d4ed8;\n                }\n\n                .engine-checkbox-wrapper {\n                    font-size: 13px;\n                    cursor: pointer;\n                    display: flex;\n                    align-items: center;\n                    gap: 6px;\n                    color: #475569;\n                    user-select: none;\n                    transition: all 0.3s;\n                    padding: 2px 6px;\n                    border-radius: 4px;\n                    border: 1px solid transparent;\n                }\n                .engine-loading {\n                    color: #2563eb !important;\n                    background: #eff6ff;\n                    border-color: #bfdbfe;\n                    animation: magnet-pulse 1.5s infinite;\n                }\n                .engine-success {\n                    color: #16a34a !important;\n                    background: #f0fdf4;\n                }\n                .engine-error {\n                    color: #dc2626 !important;\n                    background: #fef2f2;\n                }\n                @keyframes magnet-pulse {\n                    0% { opacity: 1; }\n                    50% { opacity: 0.5; }\n                    100% { opacity: 1; }\n                }\n                \n                .file-list-container {\n                    background: #f8fafc; border-radius: 6px; padding: 10px; margin-top: 12px; \n                    max-height: 300px; overflow-y: auto; border: 1px solid #e2e8f0; width: 100%; box-sizing: border-box;\n                }\n                .file-list-container::-webkit-scrollbar {\n                    width: 4px;\n                }\n                .file-list-container::-webkit-scrollbar-thumb {\n                    background: #e2e8f0;\n                    border-radius: 10px;\n                }\n                .file-list-container::-webkit-scrollbar-track {\n                    background: transparent;\n                }\n                \n                \n                .magnet-history-list {\n                    display: flex;\n                    flex-wrap: wrap;\n                    gap: 8px;\n                    margin: 0 5px 12px;\n                    align-items: center;\n                }\n                .history-item {\n                    display: inline-flex;\n                    align-items: center;\n                    background: #f1f5f9;\n                    color: #475569;\n                    padding: 1px 8px;\n                    border-radius: 4px;\n                    font-size: 11px;\n                    cursor: pointer;\n                    transition: all 0.2s;\n                }\n                .history-del-btn {\n                    margin-left: 5px;\n                    padding: 0 2px;\n                    color: #94a3b8;\n                    font-weight: bold;\n                    font-size: 12px;\n                    cursor: pointer;\n                }\n                .history-del-btn:hover {\n                    color: #ef4444;\n                }\n                .history-item:hover {\n                    background: #e2e8f0;\n                    color: #2563eb;\n                    border-color: #bfdbfe;\n                }\n                .clear-history {\n                    color: #94a3b8;\n                    font-size: 11px;\n                    cursor: pointer;\n                    align-self: center;\n                    margin-left:auto;\n                    user-select:none;\n                }\n                .clear-history:hover {\n                    color: #ef4444;\n                }\n                \n                @media (max-width: 1000px) {\n                    .magnet-info {\n                        flex-direction: column; /* 纵向排列,使子元素各自占据一行 */\n                        align-items: stretch;   /* 让子元素拉伸占满宽度 */\n                    }\n                \n                    .magnet-content {\n                        width: 100%;\n                        min-width: 100%;       /* 覆盖之前的 300px 设置 */\n                        margin-bottom: 12px;   /* 与下方的按钮组保持间距 */\n                    }\n                \n                    .magnet-actions {\n                        justify-content: flex-start; /* 按钮靠左对齐 */\n                        flex-wrap: wrap;             /* 如果按钮还是太多,允许按钮内部换行 */\n                        gap: 8px;                    /* 加大按钮间距方便点击 */\n                    }\n                }\n            </style>\n        ';
        }
        async handle() {}
        openMagnetHubDialog(carNum) {
            layer.open({
                type: 1,
                title: "磁力搜索",
                content: '<div id="magnetHubBox"></div>',
                area: [ "70%", "95%" ],
                shadeClose: !0,
                scrollbar: !1,
                anim: -1,
                success: () => {
                    this.createMagnetHub($("#magnetHubBox"), carNum).then();
                }
            });
        }
        async createMagnetHub($hubContainer, keyword) {
            if (!$hubContainer) throw new Error("未传入容器");
            let currentKeyword = (keyword || "").trim().replace("FC2-", "");
            this.currentSort = cacheManager.getItem(cacheManager.magnetHubSortType_key, this.currentSort);
            const allEngineIds = this.engineConfig.map((engine => engine.id)), savedEngines = cacheManager.getItem(cacheManager.magnetHubEngines_key, allEngineIds), tabsHtml = this.engineConfig.map((engine => `\n            <label class="engine-checkbox-wrapper" data-engine-id="${engine.id}">\n                <input type="checkbox" class="engine-checkbox" value="${engine.id}" ${savedEngines.includes(engine.id) ? "checked" : ""}>\n                <span class="engine-label-text" data-url="${engine.targetPage || ""}" data-tip="右键点击,可前往原站">${engine.label}</span>\n            </label>\n        `)).join(""), $container = $(`\n            <div class="magnet-container">\n                <div class="magnet-search-bar">\n                    <input type="text" class="magnet-input" placeholder="输入番号或关键词..." value="${currentKeyword}">\n                    <button class="search-submit-btn">搜索</button>\n                </div>\n                \n                <div id="specialHint" style="display:none;"></div>\n                \n                <div class="magnet-history-list" id="magnetHistory"></div>\n                \n                <div class="magnet-engine-selector">\n                    <div class="engine-group">${tabsHtml}</div>\n                    <div class="engine-actions">\n                        <span class="select-all" style="cursor:pointer; color:#2563eb; font-size:12px;">全选</span>\n                    </div>\n                </div>\n                <div class="magnet-tools">\n                    <span>排序方式:</span>\n                    <span class="sort-item ${"" === this.currentSort ? "active" : ""}" data-sort="">默认</span>\n                    <span class="sort-item ${"date" === this.currentSort ? "active" : ""}" data-sort="date">日期最新</span>\n                    <span class="sort-item ${"size" === this.currentSort ? "active" : ""}" data-sort="size">文件最大</span>\n                    <div style="margin-left: auto; text-align: right;">\n                        <span id="resultCount" style="color: #94a3b8; font-weight: 500;"></span>\n                        <span id="dedupTip" style="display: none; font-size: 11px; background: #f1f5f9; padding: 2px 6px; border-radius: 4px; margin-left: 5px;"></span>\n                    </div>\n                </div>\n                <div class="magnet-results"></div>\n            </div>\n        `), $resultsContainer = $container.find(".magnet-results"), $searchInput = $container.find(".magnet-input"), $historyBox = $container.find("#magnetHistory"), renderHistory = () => {
                const history = cacheManager.getItem(cacheManager.magnetHubHistory_key, []);
                if (!history.length) {
                    $historyBox.hide();
                    return;
                }
                const tagsHtml = history.map((h => `\n                <span class="history-item" data-val="${h}">\n                    ${h}\n                    <i class="history-del-btn" title="删除">×</i>\n                </span>\n            `)).join("");
                $historyBox.html('<span style="font-size: 11px; color: #94a3b8; margin-right: 4px; user-select:none;">搜索历史:</span>' + tagsHtml + '<span class="clear-history">[清空]</span>').show();
            }, saveToHistory = kw => {
                if (!kw || kw.length < 2) return;
                let history = cacheManager.getItem(cacheManager.magnetHubHistory_key, []);
                history = [ kw, ...history.filter((i => i !== kw)) ].slice(0, this.maxHistoryCount);
                cacheManager.setItem(cacheManager.magnetHubHistory_key, history);
                renderHistory();
            }, performSearch = () => {
                const newKeyword = $searchInput.val().trim();
                (kw => {
                    const $hintContainer = $container.find("#specialHint");
                    $hintContainer.empty();
                    const matchedRule = [ {
                        pattern: /^MIUM-/i,
                        replace: "300MIUM-"
                    }, {
                        pattern: /^LUXU-/i,
                        replace: "259LUXU-"
                    }, {
                        pattern: /^GANA-/i,
                        replace: "200GANA-"
                    } ].find((r => r.pattern.test(kw)));
                    if (matchedRule) {
                        const specialNum = kw.replace(matchedRule.pattern, matchedRule.replace), $tip = $(`\n                    <div style="margin: -8px 5px 10px; font-size: 12px; color: #b45309; background: #fffbeb; padding: 6px 12px; border-radius: 6px; border: 1px solid #fde68a; display: inline-block;">\n                        <span>💡 猜你想搜:</span>\n                        <strong class="try-special-num" \n                                style="cursor: pointer; text-decoration: underline; color: #2563eb; transition: color 0.2s;"\n                                onmouseover="this.style.color='#1d4ed8'" \n                                onmouseout="this.style.color='#2563eb'">\n                            ${specialNum}\n                        </strong>\n                    </div>\n                `);
                        $tip.find(".try-special-num").on("click", (() => {
                            $searchInput.val(specialNum);
                            performSearch();
                        }));
                        $hintContainer.append($tip).show();
                    } else $hintContainer.hide();
                })(newKeyword);
                if (newKeyword) {
                    saveToHistory(newKeyword);
                    this.searchAllSelected($container, newKeyword);
                } else $resultsContainer.html('<div class="magnet-loading">请输入关键词进行搜索</div>');
            };
            $container.on("click", ".history-item", (e2 => {
                const $target = $(e2.currentTarget);
                $searchInput.val($target.data("val"));
                performSearch();
            }));
            $container.on("click", ".history-del-btn", (e2 => {
                e2.stopPropagation();
                const valToRemove = $(e2.currentTarget).parent().data("val");
                let history = cacheManager.getItem(cacheManager.magnetHubHistory_key, []);
                history = history.filter((item => item !== valToRemove));
                cacheManager.setItem(cacheManager.magnetHubHistory_key, history);
                renderHistory();
            }));
            $container.on("click", ".clear-history", (() => {
                cacheManager.setItem(cacheManager.magnetHubHistory_key, []);
                renderHistory();
            }));
            $container.find(".search-submit-btn").on("click", performSearch);
            $searchInput.on("keypress", (e2 => {
                13 === e2.which && performSearch();
            }));
            $container.on("click", ".sort-item", (e2 => {
                const $target = $(e2.target);
                this.currentSort = $target.data("sort");
                cacheManager.setItem(cacheManager.magnetHubSortType_key, this.currentSort);
                $container.find(".sort-item").removeClass("active");
                $target.addClass("active");
                performSearch();
            }));
            $container.on("change", ".engine-checkbox", (() => {
                performSearch();
            }));
            $container.on("click", ".select-all", (() => {
                const allChecked = 0 === $container.find(".engine-checkbox:not(:checked)").length;
                $container.find(".engine-checkbox").prop("checked", !allChecked);
                performSearch();
            }));
            renderHistory();
            performSearch();
            $hubContainer.append($container);
            $container.off("click.copy").on("click.copy", ".magnet-copy-btn", (function(e2) {
                e2.preventDefault();
                const $btn = $(this), magnet = $btn.data("magnet");
                CommonUtil.copyToClipboard(magnet, (() => {
                    const originalText = $btn.text();
                    $btn.text("已复制");
                    setTimeout((() => {
                        $btn.text(originalText);
                    }), 1e3);
                }));
            }));
            $container.off("click.preview").on("click.preview", ".magnet-preview-btn", (async e2 => {
                e2.preventDefault();
                const magnet = $(e2.currentTarget).data("magnet"), cacheKey = "whatslink_" + magnet, cacheData = tempCacheManager.getItem(cacheKey, []);
                if (CommonUtil.isNotNull(cacheData)) {
                    window.viewerManager.showImageViewer(cacheData, {
                        toTop: !1,
                        initZoom: !1
                    });
                    return;
                }
                const loadObj = loading();
                try {
                    const url = `https://whatslink.info/api/v1/link?url=${magnet}`, res = await gmHttp.get(url);
                    if (!res || !Array.isArray(res.screenshots) || 0 === res.screenshots.length) {
                        show.error("该磁力链接暂无预览图");
                        return;
                    }
                    const imgList = [];
                    for (let item of res.screenshots) item && item.screenshot && imgList.push(item.screenshot);
                    if (CommonUtil.isNull(imgList)) {
                        show.error("未发现有效的预览图片地址");
                        return;
                    }
                    tempCacheManager.setItem(cacheKey, imgList);
                    window.viewerManager.showImageViewer(imgList, {
                        toTop: !1,
                        initZoom: !1
                    });
                } catch (e3) {
                    show.error(e3);
                    console.error(e3);
                } finally {
                    loadObj.close();
                }
            }));
            $container.on("contextmenu", ".engine-label-text", (e2 => {
                let targetUrl = $(e2.currentTarget).attr("data-url");
                if (!targetUrl) return;
                e2.preventDefault();
                const currentKw = $searchInput.val().trim();
                targetUrl = currentKw ? targetUrl.replace("{keyword}", encodeURIComponent(currentKw)) : targetUrl.replace("{keyword}", "");
                window.open(targetUrl, "_blank");
            }));
        }
        async searchAllSelected($container, keyword) {
            const ticket = ++this.currentSearchTicket, selectedIds = [];
            $container.find(".engine-checkbox:checked").each(((i, el) => selectedIds.push($(el).val())));
            cacheManager.setItem(cacheManager.magnetHubEngines_key, selectedIds);
            $container.find(".engine-checkbox-wrapper").removeClass("engine-loading engine-success engine-error");
            const $resultsContainer = $container.find(".magnet-results");
            if (0 === selectedIds.length) {
                $resultsContainer.html('\n                <div style=" display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; background: #fff5f5; border: 1px dashed #feb2b2; border-radius: 12px; margin-top: 10px; ">\n                    <svg style="width: 48px; height: 48px; color: #f56565; margin-bottom: 12px;" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>\n                    </svg>\n                    <div style="color: #c53030; font-size: 15px; font-weight: 500;">未选择搜索引擎</div>\n                    <div style="color: #f56565; font-size: 13px; margin-top: 4px;">请在上方勾选至少一个引擎以开始搜索</div>\n                </div>\n            ');
                return;
            }
            $resultsContainer.html('<div class="magnet-loading">正在聚合搜索中...</div>');
            let allResults = [], duplicateCount = 0;
            const searchPromises = selectedIds.map((async id => {
                const engine = this.engineConfig.find((e2 => e2.id === id)), $engineWrapper = $container.find(`.engine-checkbox-wrapper[data-engine-id="${id}"]`);
                $engineWrapper.addClass("engine-loading");
                try {
                    const cacheKey = `${engine.id}_${keyword}`;
                    let results = tempCacheManager.getItem(cacheKey, []);
                    if (CommonUtil.isNull(results)) {
                        const url = engine.url.replace("{keyword}", encodeURIComponent(keyword));
                        results = await engine.handler(url, keyword);
                        tempCacheManager.setItem(cacheKey, results);
                    }
                    if (ticket !== this.currentSearchTicket) return;
                    results.forEach((item => {
                        const hash = this._getMagnetHash(item.magnet), existingIndex = allResults.findIndex((existing => this._getMagnetHash(existing.magnet) === hash));
                        if (-1 === existingIndex) {
                            item.source = engine.label;
                            allResults.push(item);
                        } else {
                            duplicateCount++;
                            const existingItem = allResults[existingIndex], currentHasFileList = item.fileListHtml && item.fileListHtml.length > 0, existingHasFileList = existingItem.fileListHtml && existingItem.fileListHtml.length > 0, currentFileCount = item.fileCount || 0, existingFileCount = existingItem.fileCount || 0;
                            if (currentHasFileList && !existingHasFileList) {
                                item.source = engine.label;
                                allResults[existingIndex] = item;
                            } else if (currentFileCount > existingFileCount) {
                                item.source = engine.label;
                                allResults[existingIndex] = item;
                            }
                        }
                    }));
                    $engineWrapper.removeClass("engine-loading").addClass("engine-success");
                    this.displayResults($resultsContainer, allResults, duplicateCount);
                } catch (e2) {
                    console.error(`${engine.label} 搜索失败:`, e2);
                    const errorMessage = e2.message || "未知错误(可能是跨域或网络问题)";
                    $engineWrapper.removeClass("engine-loading").addClass("engine-error").attr("title", `错误详情: ${errorMessage}`);
                }
            }));
            await Promise.all(searchPromises);
            0 === allResults.length && ticket === this.currentSearchTicket && $resultsContainer.html('<div class="magnet-loading">未发现资源</div>');
        }
        _getMagnetHash(magnet) {
            if (!magnet) return "";
            const match = magnet.match(/xt=urn:btih:([a-fA-F0-9]{40}|[a-zA-Z2-7]{32})/i);
            return match ? match[1].toLowerCase() : magnet.toLowerCase();
        }
        displayResults($container, results, duplicateCount) {
            $container.empty();
            if (!results || 0 === results.length) {
                $container.append('<div class="magnet-loading" style="color:#c8a374;">未发现相关磁力资源</div>');
                return;
            }
            $("#resultCount").text(`共 ${results.length} 条`);
            const $dedupTip = $("#dedupTip");
            duplicateCount > 0 ? $dedupTip.text(`过滤重复: ${duplicateCount} 条`).show() : $dedupTip.hide();
            let sortedResults = [ ...results ];
            "date" === this.currentSort ? sortedResults.sort(((a, b) => {
                const timeA = this._parseRelativeDate(a.date);
                return this._parseRelativeDate(b.date) - timeA;
            })) : "size" === this.currentSort && sortedResults.sort(((a, b) => b.sizeByte - a.sizeByte));
            sortedResults.forEach((result => {
                let highlightedTitle = result.title;
                this.highlightKeywords.forEach((word => {
                    const reg = new RegExp(`(${word})`, "gi");
                    highlightedTitle = highlightedTitle.replace(reg, '<span style="color: #ef4444; font-weight: bold; background: #fee2e2; padding: 0 2px; border-radius: 2px;">$1</span>');
                }));
                const $result = $(`\n                <div class="magnet-result">\n                    <div class="magnet-info">\n                        <div class="magnet-content">\n                            <a class="magnet-title" href="${result.detailPageUrl}" target="_blank" title="${result.title}">\n                                ${highlightedTitle}\n                            </a>\n                            <div class="magnet-meta">\n                                <span style="color: #007bff; font-weight: bold;">[ ${result.source} ]</span>\n                                <span style="margin: 0 4px;">|</span>\n                                <span>📦 ${result.size || "未知"}</span>\n                                <span style="margin: 0 4px;">|</span>\n                                <span>📅 ${result.date || "未知"}</span>\n                                ${result.fileCount ? `<span style="margin: 0 4px;">|</span><span>📂 文件数 ${result.fileCount}</span>` : ""}\n                            </div>\n                        </div>\n            \n                        <div class="magnet-actions">\n                            <jhs-btn type="aliceBlue" class="magnet-preview-btn" data-magnet="${result.magnet}">预览</jhs-btn>\n                            <jhs-btn type="white" class="magnet-copy-btn" data-magnet="${result.magnet}">复制</jhs-btn>\n                            <jhs-btn type="royalBlue" class="magnet-btn-download"><a href="${result.magnet}">立即下载</a></jhs-btn>\n                            <jhs-btn type="denimBlue" class="magnet-115-btn magnet-down-115" data-magnet="${result.magnet}">115离线</jhs-btn>\n                        </div>\n                    </div>\n\n                    ${result.fileListHtml ? `<div class="file-list-container"> ${result.fileListHtml} </div> ` : ""}\n                </div>\n            `);
                $container.append($result);
            }));
        }
        async commonParse(url, keyword) {
            const baseUrl = NetUtil.getBaseUrl(url), html = await gmHttp.get(url), $dom = DomUtil.htmlTo$dom(html), results = [];
            $dom.find(".torrent-list tbody tr").each(((i, el) => {
                const $el = $(el);
                if ($el.text().includes("置顶")) return;
                const $a = $el.find("td:nth-child(2) a"), relativeUrl = $a.attr("href"), detailPageUrl = relativeUrl ? new URL(relativeUrl, baseUrl).href : "", title = $a.attr("title") || $a.text().trim();
                if (!title.toLowerCase().includes(keyword.toLowerCase())) return;
                const magnet = $el.find("td:nth-child(3) a[href^='magnet:']").attr("href"), size = $el.find("td:nth-child(4)").text().trim(), date = $el.find("td:nth-child(5)").text().trim();
                magnet && results.push({
                    detailPageUrl: detailPageUrl,
                    title: title,
                    magnet: magnet,
                    size: size,
                    date: date,
                    sizeByte: this._quickParseSize(size)
                });
            }));
            return results;
        }
        async parseBTSOW(url, keyword) {
            const data = [ {
                search: keyword
            }, 50, 1 ], dataList = (await gmHttp.postJson(url, data)).data, baseUrl = NetUtil.getBaseUrl(url), results = [];
            for (let i = 0; i < dataList.length; i++) {
                let item = dataList[i];
                const size = (item.size / 1073741824).toFixed(2) + " GB", detailPageUrl = `${baseUrl}/magnet/detail/${item.hash}`;
                results.push({
                    detailPageUrl: detailPageUrl,
                    title: item.name.replace(/<[^>]*>?/gm, ""),
                    magnet: "magnet:?xt=urn:btih:" + item.hash,
                    size: size,
                    sizeByte: this._quickParseSize(size),
                    date: DateUtil.formatDate(new Date(1e3 * item.lastUpdateTime))
                });
            }
            return results;
        }
        async parseBtdig(url, keyword) {
            const html = await gmHttp.get(url), $dom = DomUtil.htmlTo$dom(html), results = [];
            $dom.find(".one_result").each(((i, el) => {
                const $el = $(el), $link = $el.find(".torrent_name a"), detailPageUrl = $link.attr("href"), title = $link.text().trim();
                if (!title.toLowerCase().includes(keyword.toLowerCase())) return;
                const magnet = $el.find(".fa-magnet a[href^='magnet:']").attr("href"), size = $el.find(".torrent_size").text().trim(), fileCount = $el.find(".torrent_files").text().trim();
                const dateChinese = $el.find(".torrent_age").text().trim().replace(/found\s+/g, "").replace(/years?/g, "年").replace(/months?/g, "个月").replace(/weeks?/g, "周").replace(/days?/g, "天").replace(/hours?/g, "小时").replace(/minutes?/g, "分钟").replace(/ago/g, "前").replace(/\s+/g, ""), fileListHtml = $el.find(".torrent_excerpt div[class*='fa-']").map(((_, div) => {
                    const $div = $(div), text = $div.text().trim();
                    if (!text) return null;
                    const isFolder = $div.hasClass("fa-folder-open");
                    return `<div class="file-item" style="font-size: 12px; ${isFolder ? "font-weight: bold; color: #1e293b;" : "color: #475569;"} overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">\n                ${isFolder ? "📁" : "📄"} ${text}\n            </div>`;
                })).get().join("") || '<div style="font-size: 12px; color: #94a3b8;">暂无文件列表</div>';
                magnet && results.push({
                    detailPageUrl: detailPageUrl,
                    title: title,
                    magnet: magnet,
                    size: size,
                    date: dateChinese,
                    fileCount: fileCount,
                    fileListHtml: fileListHtml,
                    sizeByte: this._quickParseSize(size)
                });
            }));
            return results;
        }
        async parseCld(url, keyword) {
            const html = await gmHttp.get(url), $dom = DomUtil.htmlTo$dom(html), results = [], baseUrl = NetUtil.getBaseUrl(url);
            $dom.find(".ssbox").each(((i, el) => {
                const $el = $(el), $link = $el.find(".title h3 a"), detailPageUrl = `${baseUrl}${$link.attr("href")}`, title = $link.text().trim();
                if (!title.toLowerCase().includes(keyword.toLowerCase())) return;
                const magnet = $el.find(".sbar a[href^='magnet:']").attr("href"), date = $el.find(".sbar span:contains('添加时间') b").text().trim(), size = $el.find(".sbar span:contains('大小') b").text().trim(), fileCount = $el.find(".slist ul li").length, fileList = $el.find(".slist ul li").map(((_, li) => $(li).text().trim())).get(), fileListHtml = fileList.length > 0 ? fileList.map((name2 => `<div class="file-item" style="font-size: 12px; color: #475569; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">📄 ${name2}</div>`)).join("") : '<div style="font-size: 12px; color: #94a3b8;">暂无文件列表</div>';
                magnet && results.push({
                    detailPageUrl: detailPageUrl,
                    title: title,
                    magnet: magnet,
                    size: size,
                    date: date,
                    fileListHtml: fileListHtml,
                    fileCount: fileCount,
                    sizeByte: this._quickParseSize(size)
                });
            }));
            return results;
        }
        async parseBt4g(url, keyword) {
            const html = await gmHttp.get(url), $dom = DomUtil.htmlTo$dom(html), results = [], baseUrl = NetUtil.getBaseUrl(url);
            $dom.find(".list-group-item.result-item").each(((i, el) => {
                const $el = $(el), $link = $el.find("h5.mb-1 a"), title = $link.attr("title") || $link.text().trim(), relativeHref = $link.attr("href"), detailPageUrl = `${baseUrl}${relativeHref}`;
                if (keyword && !title.toLowerCase().includes(keyword.toLowerCase())) return;
                let magnet = relativeHref.startsWith("/magnet/") ? `magnet:?xt=urn:btih:${relativeHref.split("/").pop()}` : relativeHref;
                const date = $el.find("span:contains('Creation Time:')").text().replace(/Creation Time:\s*/i, "").trim(), size = $el.find("span:contains('Total Size:') b.cpill").text().trim(), fileCountRaw = $el.find("span:contains('Files:')").text(), fileCount = parseInt(fileCountRaw.replace(/Files:\s*/i, "").trim()) || 0, fileList = $el.find("ul li").map(((_, li) => {
                    const $li = $(li).clone();
                    $li.find("span").remove();
                    return $li.text().trim();
                })).get(), fileListHtml = fileList.length > 0 ? fileList.map((name2 => `<div class="file-item" style="font-size: 12px; color: #475569; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">📄 ${name2}</div>`)).join("") : '<div style="font-size: 12px; color: #94a3b8;">暂无文件列表</div>';
                magnet && results.push({
                    detailPageUrl: detailPageUrl,
                    title: title,
                    magnet: magnet,
                    size: size,
                    date: date,
                    fileListHtml: fileListHtml,
                    fileCount: fileCount,
                    sizeByte: this._quickParseSize(size)
                });
            }));
            return results;
        }
        _parseRelativeDate(dateStr) {
            if (!dateStr) return 0;
            try {
                return DateUtil.toTimestamp(dateStr);
            } catch (e2) {}
            const match = dateStr.match(/^(\d+)(年|个月|周|天|小时|分钟)前$/);
            if (match) {
                const value = parseInt(match[1]), unit = match[2], now = Date.now();
                switch (unit) {
                  case "分钟":
                    return now - value * DateUtil.MINUTE;

                  case "小时":
                    return now - value * DateUtil.HOUR;

                  case "天":
                    return now - value * DateUtil.DAY;

                  case "周":
                    return now - value * DateUtil.WEEK;

                  case "个月":
                    return now - value * DateUtil.MONTH;

                  case "年":
                    return now - 365 * value * DateUtil.DAY;

                  default:
                    return now;
                }
            }
            return 0;
        }
        _quickParseSize(str) {
            const num = parseFloat(str) || 0, strUpper = str.toUpperCase();
            return strUpper.includes("G") ? 1024 * num * 1024 * 1024 : strUpper.includes("M") ? 1024 * num * 1024 : strUpper.includes("K") ? 1024 * num : num;
        }
    }
    const _HotkeyManager = class {
        constructor() {
            throw new Error("工具类不可实例化");
        }
        static registerHotkey(hotkeyString, callback, keyupCallback = null) {
            if (Array.isArray(hotkeyString)) {
                let id_list = [];
                hotkeyString.forEach((hotkey => {
                    if (!this.isHotkeyFormat(hotkey)) throw new Error("快捷键格式错误");
                    let id = this.recordHotkey(hotkey, callback, keyupCallback);
                    id_list.push(id);
                }));
                return id_list;
            }
            if (!this.isHotkeyFormat(hotkeyString)) throw new Error("快捷键格式错误");
            return this.recordHotkey(hotkeyString, callback, keyupCallback);
        }
        static recordHotkey(hotkeyString, callback, keyupCallback) {
            let id = Math.random().toString(36).substr(2);
            this.registerHotKeyMap.set(id, {
                hotkeyString: hotkeyString,
                callback: callback,
                keyupCallback: keyupCallback
            });
            return id;
        }
        static unregisterHotkey(id) {
            this.registerHotKeyMap.has(id) && this.registerHotKeyMap.delete(id);
        }
        static isHotkeyFormat(hotkeyString) {
            return hotkeyString.toLowerCase().split("+").map((k => k.trim())).every((k => [ "ctrl", "shift", "alt" ].includes(k) || 1 === k.length));
        }
        static judgeHotkey(hotkeyString, event) {
            const keyList = hotkeyString.toLowerCase().split("+").map((k => k.trim())), mods_ctrl = keyList.includes("ctrl"), mods_shift = keyList.includes("shift"), mods_alt = keyList.includes("alt"), mainKey = (keyList.includes("meta") || keyList.includes("command"), 
            keyList.find((k => ![ "ctrl", "shift", "alt", "meta", "command" ].includes(k))));
            if (!mainKey) {
                const keyName = event.key.toLowerCase();
                return !(!keyList.includes("alt") || "alt" !== keyName) || (!(!keyList.includes("ctrl") || "control" !== keyName) || !(!keyList.includes("shift") || "shift" !== keyName));
            }
            const ctrlMatch = (this.isMac ? event.metaKey : event.ctrlKey) === mods_ctrl, shiftMatch = event.shiftKey === mods_shift, altMatch = event.altKey === mods_alt, keyMatch = event.key.toLowerCase() === mainKey.toLowerCase();
            return ctrlMatch && shiftMatch && altMatch && keyMatch;
        }
    };
    __publicField(_HotkeyManager, "isMac", 0 === navigator.platform.indexOf("Mac"));
    __publicField(_HotkeyManager, "registerHotKeyMap", new Map);
    __publicField(_HotkeyManager, "handleKeydown", (event => {
        const activeElement = document.activeElement;
        if (!("INPUT" === activeElement.tagName || "TEXTAREA" === activeElement.tagName || activeElement.isContentEditable)) for (const [id, data] of _HotkeyManager.registerHotKeyMap) {
            let hotkeyString = data.hotkeyString, callback = data.callback;
            _HotkeyManager.judgeHotkey(hotkeyString, event) && callback(event);
        }
    }));
    __publicField(_HotkeyManager, "handleKeyup", (event => {
        for (const [id, data] of _HotkeyManager.registerHotKeyMap) {
            let hotkeyString = data.hotkeyString, keyupCallback = data.keyupCallback;
            keyupCallback && (_HotkeyManager.judgeHotkey(hotkeyString, event) && keyupCallback(event));
        }
    }));
    let HotkeyManager = _HotkeyManager;
    document.addEventListener("keydown", (event => {
        HotkeyManager.handleKeydown(event);
    }));
    document.addEventListener("keyup", (event => {
        HotkeyManager.handleKeyup(event);
    }));
    class MenuPlugin extends BasePlugin {
        constructor() {
            super(...arguments);
            __publicField(this, "settings", {
                triggerHotkey: "Q"
            });
            __publicField(this, "mousePos", {
                x: 0,
                y: 0
            });
            __publicField(this, "$menu", null);
            __publicField(this, "$currentTarget", null);
            __publicField(this, "menuConfig", [ {
                id: "imageSearch",
                index: 1,
                label: "🖼️ 以图识图",
                handler: async () => {
                    const src = this.$currentTarget.attr("src");
                    this.getBean("ImageRecognitionPlugin").openRecognition(src);
                },
                condition: () => this.$currentTarget && this.$currentTarget.is("img")
            }, {
                id: "magnetSearch",
                index: 2,
                label: "🧲 磁力聚合",
                handler: async () => {
                    const selectedText = window.getSelection().toString().trim();
                    this.getBean("MagnetHubPlugin").openMagnetHubDialog(selectedText);
                },
                condition: () => window.getSelection().toString().trim().length > 0
            }, {
                id: "magnetExtractor",
                index: 3,
                label: "🔍 提取本页磁力",
                handler: async () => {
                    this.getBean("MagnetExtractorPlugin").startExtractor();
                },
                condition: () => {
                    const selectedText = window.getSelection().toString().trim(), isImage = this.$currentTarget && this.$currentTarget.is("img");
                    return !selectedText && !isImage;
                }
            } ]);
        }
        getName() {
            return "MenuPlugin";
        }
        getRegisterCondition() {
            return !0;
        }
        async handle() {
            await this.loadSettings();
            this.registerGMMenu();
            this.refreshHotkey();
            this.render();
            this.bindEvents();
        }
        async loadSettings() {
            const saved = await cacheManager.getItem(cacheManager.menuSetting_key);
            saved && (this.settings = saved);
        }
        refreshHotkey() {
            this.hotkeyId && HotkeyManager.unregisterHotkey(this.hotkeyId);
            this.hotkeyId = HotkeyManager.recordHotkey(this.settings.triggerHotkey, (event => {
                if (this.$menu && this.$menu.is(":visible")) {
                    const elementAtMouse = document.elementFromPoint(this.mousePos.x, this.mousePos.y), $hoveredItem = $(elementAtMouse).closest(".plugin-alt-menu li");
                    if ($hoveredItem.length > 0) {
                        $hoveredItem.click();
                        return;
                    }
                    this.hideMenu();
                } else {
                    event.preventDefault();
                    this.hideMenu();
                    this.showMenu(this.mousePos.x, this.mousePos.y);
                }
            }));
        }
        registerGMMenu() {
            this.gmMenuId && GM_unregisterMenuCommand(this.gmMenuId);
            this.gmMenuId = GM_registerMenuCommand(`⚙️ 设置触发快捷键 (当前: ${this.settings.triggerHotkey})`, (() => {
                this.openHotkeySetter();
            }));
        }
        openHotkeySetter() {
            const content = `\n            <style>\n                .hotkey-container {\n                    padding: 24px;\n                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;\n                    background-color: #fff;\n                }\n                .hotkey-label {\n                    margin-bottom: 12px;\n                    color: #8c8c8c;\n                    font-size: 13px;\n                    line-height: 1.5;\n                }\n                .hotkey-input-group {\n                    display: flex;\n                    align-items: center;\n                    gap: 12px;\n                }\n                #triggerHotkey {\n                    flex: 1;\n                    height: 40px;\n                    text-align: center;\n                    font-weight: 600;\n                    font-size: 14px;\n                    color: #409eff;\n                    background-color: #f5f7fa;\n                    border: 1px solid #dcdfe6;\n                    border-radius: 6px;\n                    cursor: pointer;\n                    transition: all 0.2s cubic-bezier(.645,.045,.355,1);\n                    outline: none;\n                }\n                #triggerHotkey:hover {\n                    border-color: #c0c4cc;\n                }\n                #triggerHotkey:focus {\n                    border-color: #409eff;\n                    background-color: #ecf5ff;\n                    box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);\n                }\n                #triggerHotkey::placeholder {\n                    color: #a8abb2;\n                    font-weight: normal;\n                }\n                #clearHotkey {\n                    height: 40px;\n                    padding: 0 16px;\n                    font-size: 13px;\n                    color: #606266;\n                    background: #fff;\n                    border: 1px solid #dcdfe6;\n                    border-radius: 6px;\n                    cursor: pointer;\n                    transition: all 0.2s;\n                }\n                #clearHotkey:hover {\n                    color: #ff4d4f;\n                    border-color: #ff4d4f;\n                    background-color: #fff1f0;\n                }\n                #clearHotkey:active {\n                    background-color: #ffccc7;\n                }\n            </style>\n            <div class="hotkey-container">\n                <div class="hotkey-label">\n                    快捷键录入:请直接在方框内按下组合键<br>\n                    <span style="font-size: 11px; color: #c0c4cc;">支持 Ctrl, Shift, Alt, Meta 与普通键组合</span>\n                </div>\n                <div class="hotkey-input-group">\n                    <input type="text" id="triggerHotkey" \n                        value="${this.settings.triggerHotkey}" \n                        readonly \n                        placeholder="点击此处按下按键..."\n                        autocomplete="off">\n                    <button type="button" id="clearHotkey">清空</button>\n                </div>\n            </div>\n        `;
            layer.open({
                type: 1,
                title: "⚙️ 设置触发快捷键",
                area: [ "400px", "250px" ],
                content: content,
                btn: [ "确定", "取消" ],
                success: (layero, index) => {
                    const $input = layero.find("#triggerHotkey"), $clearBtn = layero.find("#clearHotkey");
                    $input.focus();
                    $clearBtn.on("click", (() => $input.val("")));
                    $input.on("keydown", (event => {
                        event.preventDefault();
                        event.stopPropagation();
                        const hotkey = this.parseHotkey(event);
                        /[\u4e00-\u9fa5]/.test(hotkey) ? show.error("非法输入:不能输入中文或输入法转换错误") : $input.val(hotkey);
                    }));
                },
                yes: (index, layero) => {
                    const newKey = layero.find("#triggerHotkey").val();
                    if (newKey) {
                        this.settings.triggerHotkey = newKey;
                        cacheManager.setItem(cacheManager.menuSetting_key, this.settings);
                        this.refreshHotkey();
                        this.registerGMMenu();
                        layer.close(index);
                        show.ok("设置已生效");
                    } else show.error("快捷键不能为空");
                }
            });
        }
        parseHotkey(event) {
            if ("Backspace" === event.key || "Process" === event.key) return "";
            const keys = [];
            event.ctrlKey && keys.push("Ctrl");
            event.shiftKey && keys.push("Shift");
            event.altKey && keys.push("Alt");
            event.metaKey && keys.push("Cmd");
            const key = {
                " ": "Space",
                Control: "Ctrl",
                Meta: "Cmd",
                ArrowUp: "Up",
                ArrowDown: "Down",
                ArrowLeft: "Left",
                ArrowRight: "Right"
            }[event.key] || (event.key.length > 1 ? event.key.replace("Arrow", "") : event.key.toUpperCase());
            [ "Control", "Shift", "Alt", "Meta" ].includes(event.key) || keys.includes(key) || keys.push(key);
            return keys.join("+");
        }
        async initCss() {
            return "\n        <style>\n            .plugin-alt-menu, .plugin-submenu {\n                position: fixed; \n                z-index: 10001; \n                background: rgba(255, 255, 255, 0.85); /* 半透明背景 */\n                backdrop-filter: blur(12px); /* 毛玻璃特效 */\n                -webkit-backdrop-filter: blur(12px);\n                border: 1px solid rgba(255, 255, 255, 0.3); /* 柔和边框 */\n                border-radius: 10px; /* 大圆角 */\n                box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.12), \n                            0 1px 2px 0 rgba(0, 0, 0, 0.05); /* 层次感阴影 */\n                display: none; \n                list-style: none; \n                padding: 6px; \n                margin: 0;\n                min-width: 180px; \n                font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n                user-select: none;\n                animation: menu-fade-in 0.15s ease-out; /* 弹出动画 */\n            }\n\n            @keyframes menu-fade-in {\n                from { opacity: 0; transform: translateY(5px) scale(0.98); }\n                to { opacity: 1; transform: translateY(0) scale(1); }\n            }\n\n            .plugin-alt-menu li {\n                position: relative; \n                padding: 10px 14px; \n                font-size: 14px;\n                color: #303133; \n                cursor: pointer; \n                white-space: nowrap;\n                display: flex; \n                justify-content: space-between; \n                align-items: center;\n                border-radius: 6px; /* 每一项也有圆角 */\n                transition: all 0.2s ease;\n                margin-bottom: 2px;\n            }\n\n            .plugin-alt-menu li:last-child { margin-bottom: 0; }\n\n            .plugin-alt-menu li:hover { \n                background-color: rgba(64, 158, 255, 0.1); /* 轻微蓝色背景 */\n                color: #409eff; \n                padding-left: 18px; /* 悬停时的小位移 */\n            }\n\n            /* 带有二级菜单的箭头样式 */\n            .has-children::after { \n                content: ''; /* 使用细体箭头符号或图标 */\n                font-family: serif;\n                font-size: 12px; \n                color: #909399; \n                opacity: 0.6;\n            }\n\n            .plugin-alt-menu li:hover > .plugin-submenu {\n                display: block; \n                position: absolute; \n                left: calc(100% + 4px); \n                top: -6px;\n            }\n\n            /* 分隔线优化 */\n            .menu-divider { \n                height: 1px; \n                background: linear-gradient(to right, transparent, rgba(0,0,0,0.06), transparent);\n                margin: 6px 10px; \n            }\n\n            /* 响应式:暗色模式适配(可选) */\n            @media (prefers-color-scheme: dark) {\n                .plugin-alt-menu, .plugin-submenu {\n                    background: rgba(40, 44, 52, 0.8);\n                    border: 1px solid rgba(255, 255, 255, 0.1);\n                    color: #e0e0e0;\n                }\n                .plugin-alt-menu li { color: #e0e0e0; }\n                .plugin-alt-menu li:hover { background-color: rgba(255, 255, 255, 0.1); }\n                .menu-divider { background: rgba(255,255,255,0.1); }\n            }\n        </style>\n    ";
        }
        renderMenu($container, items) {
            items.forEach((item => {
                if ("divider" === item.type) {
                    $container.append('<div class="menu-divider"></div>');
                    return;
                }
                const $li = $(`<li class="${item.children ? "has-children" : ""}" \n                        data-id="${item.id || ""}" \n                        data-index="${item.index}"> \n                        <span>${item.label}</span> \n                      </li>`);
                item.condition && $li.data("menu-condition", item.condition);
                item.handler && $li.data("menu-handler", item.handler);
                if (item.children) {
                    const $submenu = $('<ul class="plugin-submenu"></ul>');
                    this.renderMenu($submenu, item.children);
                    $li.append($submenu);
                }
                $container.append($li);
            }));
        }
        render() {
            if (!$(".plugin-alt-menu").length) {
                this.$menu = $('<ul class="plugin-alt-menu"></ul>').appendTo("body");
                this.renderMenu(this.$menu, this.menuConfig);
            }
        }
        bindEvents() {
            let ticking = !1;
            $(document).on("mousemove", (e2 => {
                if (!ticking) {
                    window.requestAnimationFrame((() => {
                        this.mousePos.x = e2.clientX;
                        this.mousePos.y = e2.clientY;
                        ticking = !1;
                    }));
                    ticking = !0;
                }
            }));
            const debouncedEntry = DomUtil.debounce((e2 => {
                const $target = $(e2.target);
                $target.closest(".plugin-alt-menu").length || (this.$currentTarget = $target);
            }), 200);
            $(document).on("mouseenter", "*", (e2 => {
                e2.stopPropagation();
                debouncedEntry(e2);
            })).on("mouseleave", "*", (() => {
                $(e.target).closest(".plugin-alt-menu").length || (this.$currentTarget = null);
            }));
            document.addEventListener("mousedown", (e2 => {
                $(e2.target).closest(".plugin-alt-menu").length || this.hideMenu();
            }));
            $(document).on("mouseup", (e2 => {
                $(e2.target).closest(".plugin-alt-menu").length || setTimeout((() => {
                    const selection = window.getSelection();
                    if (selection.toString().trim() && selection.rangeCount > 0) {
                        const rect = selection.getRangeAt(0).getBoundingClientRect();
                        this.$currentTarget = $(selection.anchorNode.parentElement);
                        const x = rect.left, y = rect.bottom + 5;
                        this.showMenu(x, y);
                    }
                }), 10);
            }));
            this.$menu.off("click").on("click", "li", (e2 => {
                e2.stopPropagation();
                const handler = $(e2.currentTarget).data("menu-handler");
                if (handler && "function" == typeof handler) {
                    handler();
                    this.hideMenu();
                }
            }));
            window.addEventListener("scroll", (() => {
                this.$menu && this.$menu.is(":visible") && this.hideMenu();
            }), {
                capture: !0,
                passive: !0
            });
        }
        showMenu(x, y) {
            if (!this.$menu) return;
            const selectedText = window.getSelection().toString().trim(), isImage = this.$currentTarget && this.$currentTarget.is("img"), $items = this.$menu.children("li").get();
            $items.sort(((a, b) => $(a).data("index") - $(b).data("index")));
            this.$menu.append($items);
            if (isImage) {
                const $imageItem = this.$menu.find('[data-id="imageSearch"]');
                this.$menu.prepend($imageItem);
            } else if (selectedText) {
                const $magnetItem = this.$menu.find('[data-id="magnetSearch"]');
                this.$menu.prepend($magnetItem);
            }
            const noContext = !selectedText && !isImage;
            this.$menu.find("li").each(((index, el) => {
                const $el = $(el), condition = $el.data("menu-condition");
                noContext ? $el.show() : condition ? condition() ? $el.show() : $el.hide() : $el.show();
            }));
            this.$menu.css({
                visibility: "hidden",
                display: "block"
            });
            const menuWidth = this.$menu.outerWidth(), menuHeight = this.$menu.outerHeight();
            let finalX = x, finalY = y;
            finalX + menuWidth > window.innerWidth && (finalX = window.innerWidth - menuWidth - 10);
            finalY + menuHeight > window.innerHeight && (finalY = y - menuHeight - 10);
            this.$menu.css({
                top: Math.max(0, finalY),
                left: Math.max(0, finalX),
                visibility: "visible"
            }).show();
        }
        hideMenu() {
            this.$menu && this.$menu.hide();
        }
    }
    const currentHref = window.location.href;
    class ImageRecognitionPlugin extends BasePlugin {
        constructor() {
            super(...arguments);
            __publicField(this, "siteList", [ {
                name: "Google旧版",
                url: "https://www.google.com/searchbyimage?image_url={占位符}&client=firefox-b-d",
                ico: "https://www.google.com/favicon.ico"
            }, {
                name: "Google",
                url: "https://lens.google.com/uploadbyurl?url={占位符}",
                ico: "https://www.google.com/favicon.ico"
            }, {
                name: "Yandex",
                url: "https://yandex.ru/images/search?rpt=imageview&url={占位符}",
                ico: "https://yandex.ru/favicon.ico"
            } ]);
            __publicField(this, "isUploading", !1);
            __publicField(this, "MAX_HISTORY", 12);
            __publicField(this, "autoOpenEnabled", cacheManager.getItem(cacheManager.image_recognition_auto_open_key, "yes"));
        }
        getName() {
            return "ImageRecognitionPlugin";
        }
        getRegisterCondition() {
            return !0;
        }
        async initCss() {
            return "\n            <style>\n                #upload-area {\n                    border: 2px dashed #85af68;\n                    border-radius: 8px;\n                    padding: 40px;\n                    text-align: center;\n                    margin-bottom: 20px;\n                    transition: all 0.3s;\n                    background-color: #f9f9f9;\n                }\n                #upload-area:hover {\n                    border-color: #76b947;\n                    background-color: #f0f0f0;\n                }\n                /* 拖拽进入 */\n                #upload-area.highlight {\n                    border-color: #2196F3;\n                    background-color: #e3f2fd;\n                }\n                \n                \n                #select-image-btn {\n                    background-color: #4CAF50;\n                    color: white;\n                    border: none;\n                    padding: 10px 20px;\n                    border-radius: 4px;\n                    cursor: pointer;\n                    font-size: 16px;\n                    transition: background-color 0.3s;\n                }\n                #select-image-btn:hover {\n                    background-color: #45a049;\n                }\n                \n                .search-img-site-btns-container {\n                    display: flex;\n                    flex-wrap: wrap;\n                    gap: 10px;\n                    margin-top: 15px;\n                }\n                .search-img-site-btn {\n                    display: flex;\n                    align-items: center;\n                    padding: 8px 12px;\n                    background-color: #f5f5f5;\n                    border-radius: 4px;\n                    text-decoration: none;\n                    color: #333;\n                    transition: all 0.2s;\n                    font-size: 14px;\n                    border: 1px solid #ddd;\n                }\n                .search-img-site-btn:hover {\n                    background-color: #e0e0e0;\n                    transform: translateY(-2px);\n                    box-shadow: 0 2px 5px rgba(0,0,0,0.1);\n                }\n                .search-img-site-btn img {\n                    width: 16px;\n                    height: 16px;\n                    margin-right: 6px;\n                }\n                .search-img-site-btn span {\n                    white-space: nowrap;\n                }\n                \n                .history-title { font-weight: bold; margin-bottom: 10px; display: flex; justify-content: space-between; }\n\n                .history-container { \n                    margin-top: 25px; \n                    border-top: 1px solid #eee; \n                    padding-top: 15px; \n                }\n                .history-title {\n                    font-weight: bold;\n                    font-size: 15px;\n                    color: #333;\n                    margin-bottom: 15px;\n                    display: flex;\n                    justify-content: space-between;\n                    align-items: center;\n                }\n                .history-list { \n                    display: grid;\n                    grid-template-columns: repeat(4, 1fr); \n                    gap: 10px; /* 图片之间的间距 */\n                }\n                .recognition-history-item { \n                    aspect-ratio: 1 / 1; /* 保持正方形 */\n                    border-radius: 8px; \n                    cursor: pointer; \n                    border: 1px solid #ddd; \n                    background-color: #f9f9f9; \n                    overflow: hidden; \n                    transition: all 0.2s ease-in-out;\n                    display: flex;\n                    align-items: center;\n                    justify-content: center;\n                    position: relative;\n                }\n                .recognition-history-item:hover { \n                    border-color: #2196F3; \n                    box-shadow: 0 4px 12px rgba(33, 150, 243, 0.2);\n                    transform: translateY(-3px);\n                }\n                .recognition-history-item img { \n                    width: 100%; \n                    height: 100%; \n                    object-fit: contain; /* 保证图片完整显示 */\n                    display: block;\n                }\n                .delete-recognition-history-item {\n                    position: absolute;\n                    top: 2px;\n                    right: 2px;\n                    width: 20px;\n                    height: 20px;\n                    background: rgba(0, 0, 0, 0.5);\n                    color: white;\n                    border-radius: 50%;\n                    display: flex;\n                    align-items: center;\n                    justify-content: center;\n                    font-size: 16px;\n                    line-height: 1;\n                    cursor: pointer;\n                    opacity: 0;\n                    transition: opacity 0.2s;\n                    z-index: 10;\n                }\n                .recognition-history-item:hover .delete-recognition-history-item {\n                    opacity: 1;\n                }\n                .delete-recognition-history-item:hover {\n                    background: #f44336;\n                }\n                .clear-history {\n                    color: #f44336;\n                    cursor: pointer;\n                    font-size: 13px;\n                    font-weight: normal;\n                    padding: 2px 8px;\n                    border-radius: 4px;\n                }\n                .clear-history:hover {\n                    background-color: #ffebee;\n                }\n                \n                #search-results {\n                    margin-top: 20px;\n                    padding: 15px;\n                    background: #fcfcfc;\n                    border-radius: 8px;\n                    border: 1px solid #eee;\n                }\n                .search-header {\n                    display: flex;\n                    justify-content: space-between;\n                    align-items: center;\n                    margin-bottom: 12px;\n                    padding: 0 5px;\n                }\n                .search-header-title {\n                    font-size: 14px;\n                    color: #666;\n                    font-weight: 500;\n                }\n                #openAll {\n                    color: #2196F3;\n                    font-size: 13px;\n                    text-decoration: none;\n                    padding: 4px 10px;\n                    border: 1px solid #2196F3;\n                    border-radius: 4px;\n                    transition: all 0.2s;\n                }\n                #openAll:hover {\n                    background-color: #2196F3;\n                    color: #fff !important;\n                }\n                .search-img-site-btn {\n                    user-select: none;\n                }\n                .site-checkbox {\n                    cursor: pointer;\n                    width: 14px;\n                    height: 14px;\n                    margin-right: 5px;\n                    accent-color: #2196F3; /* 现代浏览器自定义勾选颜色 */\n                }\n                \n                .auto-open-wrapper {\n                    display: flex;\n                    align-items: center;\n                    cursor: pointer;\n                    user-select: none;\n                    font-size: 13px;\n                    color: #666;\n                    margin-right: 12px;\n                }\n                .auto-open-wrapper input {\n                    cursor: pointer;\n                    width: 14px;\n                    height: 14px;\n                    margin-right: 5px;\n                    accent-color: #2196F3; /* 现代浏览器自定义勾选颜色 */\n                }\n                .auto-open-wrapper:hover {\n                    color: #2196F3;\n                }\n            </style>\n        ";
        }
        getFullUrl(path) {
            if (!path) return "";
            if (/^(https?:|data:)/i.test(path)) return path;
            if (path.startsWith("/")) return window.location.origin + path;
            return window.location.href.substring(0, window.location.href.lastIndexOf("/") + 1) + path;
        }
        openRecognition(imgSrc) {
            let html = `\n            <div style="padding: 20px">\n                <div id="upload-area">\n                    <div style="color: #555;margin-bottom: 15px;">\n                        <p>拖拽图片到此处 或 点击按钮选择图片</p>\n                        <p>也可以直接 Ctrl+V 粘贴图片</p>\n                    </div>\n                    <button id="select-image-btn">选择图片</button>\n                    <input type="file" style="display: none" id="image-file" accept="image/*">\n                </div>\n                \n                <div style="text-align: center;">\n                    <img id="preview-image" alt="" src="" style="max-width: 100%; max-height: 300px; border-radius: 4px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">\n                    \n                    <div id="error-area" style="display: none; margin-top: 15px; padding: 10px; background: #fff1f0; border: 1px solid #ffa39e; border-radius: 4px;">\n                        <p id="error-message" style="color: #f5222d; margin-bottom: 10px; font-size: 14px;"></p>\n                        <button id="retry-btn" style="background-color: #faad14; color: white; border: none; padding: 5px 15px; border-radius: 4px; cursor: pointer;">重新尝试</button>\n                    </div>\n                    \n                    <div id="search-results" style="display: none;">\n                        <div class="search-header">\n                            <span class="search-header-title">选择识图网站</span>\n                            <div style="display: flex; align-items: center; gap: 10px;">\n                                <label class="auto-open-wrapper">\n                                    <input type="checkbox" id="auto-open-toggle" ${"yes" === this.autoOpenEnabled ? "checked" : ""} >\n                                    <span>完成后自动打开</span>\n                                </label>\n                                <a id="openAll" title="打开所有已勾选的引擎">全部打开</a>\n                            </div>\n                        </div>\n                        <div class="search-img-site-btns-container" id="search-img-site-btns-container"></div>\n                    </div>\n                </div>\n                \n                <div class="history-container" id="history-container" style="display: none;">\n                    <div class="history-title">\n                        最近搜索\n                        <span class="clear-history" id="clear-history">清空</span>\n                    </div>\n                    <div class="history-list" id="history-list"></div>\n                </div>  \n                \n            </div>\n        `;
            layer.open({
                type: 1,
                title: "以图识图",
                content: html,
                area: [ "50%", "95%" ],
                shadeClose: !0,
                scrollbar: !1,
                anim: -1,
                success: async layero => {
                    this.initEventListeners(layero);
                    this.renderHistory();
                    if (imgSrc) {
                        const fullSrc = this.getFullUrl(imgSrc);
                        $("#preview-image").attr("src", fullSrc);
                        this.searchByImage();
                    }
                },
                end: () => {
                    $(document).off("paste.searchImg");
                }
            });
        }
        initEventListeners($layero) {
            const $fileInput = $("#image-file");
            $layero.on("dragover", "#upload-area", (e2 => {
                e2.preventDefault();
                $(e2.currentTarget).addClass("highlight");
            }));
            $layero.on("dragleave drop", "#upload-area", (e2 => {
                e2.preventDefault();
                $(e2.currentTarget).removeClass("highlight");
                if ("drop" === e2.type) {
                    const files = e2.originalEvent.dataTransfer.files;
                    files && files[0] && this.handleImageFile(files[0]);
                }
            }));
            $layero.on("click", (e2 => {
                const $target = $(e2.target).closest("button, a, #clear-history, #retry-btn, #select-image-btn, #openAll");
                if (!$target.length) return;
                const id = $target.attr("id");
                if ("select-image-btn" === id) $fileInput.trigger("click"); else if ("openAll" === id) {
                    let firstTabOpened = !1;
                    $layero.find(".search-img-site-btn").each((function() {
                        if ($(this).find(".site-checkbox").is(":checked")) {
                            const url = $(this).attr("href");
                            if (firstTabOpened) GM_openInTab(url, {
                                insert: 0,
                                active: !1
                            }); else {
                                GM_openInTab(url, {
                                    insert: 0,
                                    active: !0
                                });
                                firstTabOpened = !0;
                            }
                        }
                    }));
                } else if ("clear-history" === id) {
                    e2.stopPropagation();
                    layer.confirm("确认清空所有搜索历史吗?", {
                        icon: 3,
                        title: "提示"
                    }, (index => {
                        cacheManager.setItem(cacheManager.image_recognition_history_key, []);
                        this.renderHistory();
                        layer.close(index);
                    }));
                } else "retry-btn" === id && this.searchByImage().then();
            }));
            $layero.on("change", "#image-file", (e2 => {
                e2.target.files && e2.target.files[0] && this.handleImageFile(e2.target.files[0]);
            }));
            $(document).off("paste.searchImg").on("paste.searchImg", (async e2 => {
                const items = e2.originalEvent.clipboardData.items;
                for (let i = 0; i < items.length; i++) if (-1 !== items[i].type.indexOf("image")) {
                    const blob = items[i].getAsFile();
                    this.handleImageFile(blob);
                    break;
                }
            }));
            $layero.on("change", "#auto-open-toggle", (e2 => {
                this.autoOpenEnabled = $(e2.target).is(":checked") ? "yes" : "no";
                cacheManager.setItem(cacheManager.image_recognition_auto_open_key, this.autoOpenEnabled);
            }));
        }
        handleImageFile(file) {
            const $previewImage = $("#preview-image");
            if (!file.type.match("image.*")) {
                show.info("请选择图片文件");
                return;
            }
            const reader = new FileReader;
            reader.onload = e2 => {
                $previewImage.attr("src", e2.target.result);
                $previewImage.attr("data-original", e2.target.result);
                this.searchByImage().then();
            };
            reader.readAsDataURL(file);
        }
        async searchByImage() {
            if (this.isUploading) return;
            const $previewImage = $("#preview-image"), imageSrc = $previewImage.attr("data-original") || $previewImage.attr("src");
            if (!imageSrc) {
                show.info("请粘贴或上传图片");
                return;
            }
            let loadObj = loading();
            const $searchResults = $("#search-results"), $siteBtnsContainer = $("#search-img-site-btns-container"), $errorArea = $("#error-area"), $errorMessage = $("#error-message");
            this.isUploading = !0;
            $searchResults.hide();
            $errorArea.hide();
            try {
                let finalImgUrl = this.getHistoryUrl(imageSrc);
                if (!finalImgUrl) {
                    let uploadData = imageSrc;
                    if (imageSrc.startsWith("http")) {
                        show.info("正在处理远程图片...");
                        uploadData = await this.fetchImageAsBase64(imageSrc);
                    }
                    show.info("开始上传图片...");
                    finalImgUrl = await async function(base64Data) {
                        var _a;
                        if ("string" != typeof base64Data || !base64Data.includes(";base64,")) {
                            console.error("无效的 Base64 数据");
                            return null;
                        }
                        const cleanBase64 = base64Data.replace(/^data:image\/\w+;base64,/, ""), formData = new FormData;
                        formData.append("image", cleanBase64);
                        formData.append("type", "base64");
                        formData.append("name", "image.png");
                        const data = await gmHttp.postFormData("https://api.imgur.com/3/upload?client_id=d70305e7c3ac5c6", formData, {
                            Referer: "https://imgur.com/"
                        });
                        if (data && data.success) return data.data.link;
                        throw new Error((null == (_a = data.data) ? void 0 : _a.error) || "上传失败");
                    }(uploadData);
                }
                if (!finalImgUrl) throw new Error("图床接口返回地址为空!");
                this.saveToHistory(imageSrc, finalImgUrl);
                $searchResults.show();
                $siteBtnsContainer.empty().show();
                const selectedSites = cacheManager.getItem(cacheManager.image_recognition_site_key, {});
                this.siteList.forEach((site => {
                    const siteUrl = site.url.replace("{占位符}", encodeURIComponent(finalImgUrl)), isChecked = !1 !== selectedSites[site.name], $btn = $(`\n                <a href="${siteUrl}" class="search-img-site-btn" target="_blank" title="${site.name}">\n                    <input type="checkbox" class="site-checkbox" data-site-name="${site.name}" \n                           style="margin-right: 5px" ${isChecked ? "checked" : ""}>\n                    <img src="${site.ico}" alt="${site.name}">\n                    <span>${site.name}</span>\n                </a>\n            `);
                    $siteBtnsContainer.append($btn);
                }));
                $siteBtnsContainer.off("change").on("change", ".site-checkbox", (function(e2) {
                    e2.stopPropagation();
                    const siteName = $(this).data("site-name"), currentSelected = cacheManager.getItem(cacheManager.image_recognition_site_key, {});
                    currentSelected[siteName] = $(this).is(":checked");
                    cacheManager.setItem(cacheManager.image_recognition_site_key, currentSelected);
                }));
                "yes" === this.autoOpenEnabled && setTimeout((() => {
                    $("#openAll").trigger("click");
                }), 200);
                return finalImgUrl;
            } catch (error) {
                show.error(error);
                console.error("[以图识图] 发生错误:", error);
                $errorMessage.text(`识别失败:${error.message || "未知错误"}`);
                $errorArea.show();
            } finally {
                this.isUploading = !1;
                loadObj.close();
            }
        }
        async fetchImageAsBase64(url) {
            return new Promise(((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: url,
                    responseType: "blob",
                    headers: {
                        Referer: window.location.href,
                        "User-Agent": navigator.userAgent
                    },
                    onload: function(response) {
                        if (200 !== response.status) {
                            reject(new Error(`无法读取远程图片,链接: ${url} 状态码: ${response.status}`));
                            return;
                        }
                        const blob = response.response, reader = new FileReader;
                        reader.onloadend = () => resolve(reader.result);
                        reader.onerror = reject;
                        reader.readAsDataURL(blob);
                    },
                    onerror: function(err) {
                        reject(err);
                    }
                });
            }));
        }
        getHistoryUrl(imageSrc) {
            const currentKey = imageSrc.startsWith("data:") ? imageSrc.substring(0, 100) : imageSrc, cachedItem = cacheManager.getItem(cacheManager.image_recognition_history_key, []).find((item => item.original === currentKey || item.uploaded === imageSrc));
            return null == cachedItem ? void 0 : cachedItem.uploaded;
        }
        saveToHistory(originalUrl, uploadedUrl) {
            let history = cacheManager.getItem(cacheManager.image_recognition_history_key, []);
            const safeOriginal = originalUrl.startsWith("data:") ? originalUrl.substring(0, 100) : originalUrl;
            history = history.filter((item => item.original !== safeOriginal && item.uploaded !== uploadedUrl));
            history.unshift({
                original: safeOriginal,
                uploaded: uploadedUrl
            });
            history.length > this.MAX_HISTORY && (history = history.slice(0, this.MAX_HISTORY));
            cacheManager.setItem(cacheManager.image_recognition_history_key, history);
            this.renderHistory();
        }
        renderHistory() {
            const history = cacheManager.getItem(cacheManager.image_recognition_history_key, []), $container = $("#history-container"), $list = $("#history-list");
            if (history && 0 !== history.length) {
                $container.show();
                $list.empty();
                history.forEach((item => {
                    const displayUrl = item.uploaded, $item = $(`\n                <div class="recognition-history-item">\n                    <div class="delete-recognition-history-item" title="删除">×</div>\n                    <img src="${displayUrl}" loading="lazy" referrerpolicy="no-referrer" alt="${displayUrl}">\n                </div>\n            `);
                    $item.on("click", (() => {
                        $("#preview-image").attr("src", item.uploaded).attr("data-original", item.original);
                        this.searchByImage().then();
                    }));
                    $item.find(".delete-recognition-history-item").on("click", (e2 => {
                        e2.stopPropagation();
                        this.deleteHistoryItem(item);
                    }));
                    $list.append($item);
                }));
            } else $container.hide();
        }
        deleteHistoryItem(item) {
            layer.confirm("确认删除这条搜索记录吗?", {
                icon: 3,
                title: "提示"
            }, (index => {
                let history = cacheManager.getItem(cacheManager.image_recognition_history_key, []);
                history = history.filter((data => data.original !== item.original));
                cacheManager.setItem(cacheManager.image_recognition_history_key, history);
                this.renderHistory();
                layer.close(index);
            }));
        }
    }
    class SeHuaTangPlugin extends BasePlugin {
        constructor() {
            super(...arguments);
            __publicField(this, "currentImageIndex", 0);
            __publicField(this, "currentImageGroup", []);
            __publicField(this, "processedArticles", new Set);
        }
        getName() {
            return "SeHuaTangPlugin";
        }
        getRegisterCondition() {
            return currentHref.includes("sehuatang") || $("title").text().includes("色花堂");
        }
        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 $enter = $(".enter-btn");
            $enter.length > 0 && $enter[0].click();
            if (!window.location.href.includes("viewthread")) {
                CommonUtil.loopDetector((() => $(".s.xst").length > 0), (() => {
                    this.parseArticleImg().then();
                }));
                CommonUtil.loopDetector((() => document.querySelector("#threadlisttableid")), (() => {
                    this.checkDom();
                }));
                this.handleImg();
            }
        }
        checkDom() {
            const targetNode = document.querySelector("#threadlisttableid");
            if (!targetNode) {
                console.error("没有找到容器节点", targetNode);
                return;
            }
            const observer = new MutationObserver((async mutations => {
                observer.disconnect();
                try {
                    this.parseArticleImg().then();
                } finally {
                    observer.observe(targetNode, config);
                }
            })), config = {
                childList: !0,
                subtree: !1
            };
            observer.observe(targetNode, config);
        }
        async parseArticleImg() {
            let allArticleImages = {};
            const cachedData = localStorage.getItem("articleImagesCache");
            cachedData && (allArticleImages = JSON.parse(cachedData));
            $(".s.xst").each((async (index, ele) => {
                const articleUrl = $(ele).attr("href");
                if (allArticleImages[articleUrl]) {
                    const $tbody = $(ele).closest("tbody");
                    $tbody.find(".imageBox").length || $tbody.append(allArticleImages[articleUrl]);
                    this.processedArticles.add(articleUrl);
                } else if (!this.processedArticles.has(articleUrl)) {
                    this.processedArticles.add(articleUrl);
                    try {
                        const $tbody = $(ele).closest("tbody");
                        if ($tbody.find(".imageBox").length) return;
                        if (!$tbody.is(":visible")) return;
                        const res = await fetch(articleUrl);
                        if (!res.ok) return;
                        const imgs = $($.parseHTML(await res.text())).find("img.zoom[file]:not([file*='static'], [file*='hrline'])").slice(0, 5);
                        if (!imgs.length) return;
                        const fullHTML = `\n                <tr class="imageBox">\n                    <td colspan="5">\n                        <div style="display:flex;gap:10px;overflow-x:auto;padding:5px 0">${imgs.map(((_, img) => `<img src="${$(img).attr("file")}" loading="lazy" 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            `;
                        allArticleImages[articleUrl] = fullHTML;
                        localStorage.setItem("articleImagesCache", JSON.stringify(allArticleImages));
                        $tbody.append(fullHTML);
                    } catch (e2) {
                        console.error("Error:", articleUrl, e2);
                    }
                }
            }));
        }
        handleImg() {
            document.addEventListener("click", (event => {
                if ("IMG" === event.target.tagName && event.target.closest(".imageBox")) {
                    const previewTbody = event.target.closest(".imageBox");
                    this.currentImageGroup = Array.from(previewTbody.querySelectorAll("img"));
                    this.currentImageIndex = this.currentImageGroup.indexOf(event.target);
                    this.createNavigateBtn();
                }
            }));
        }
        createNavigateBtn() {
            CommonUtil.loopDetector((() => $("#imgzoom_picpage").length > 0), (() => {
                if (0 === $("#imgzoom_picpage").length) return;
                const zoomContainer = document.getElementById("imgzoom_picpage");
                if (!zoomContainer) return;
                zoomContainer.querySelectorAll("#zimg_prev, #zimg_next").forEach((btn => btn.remove()));
                const prevBtn = document.createElement("div");
                prevBtn.id = "zimg_prev";
                prevBtn.className = "zimg_prev";
                prevBtn.onclick = () => this.navigateImage(-1);
                const nextBtn = document.createElement("div");
                nextBtn.id = "zimg_next";
                nextBtn.className = "zimg_next";
                nextBtn.onclick = () => this.navigateImage(1);
                zoomContainer.append(prevBtn, nextBtn);
            }));
        }
        navigateImage(direction) {
            this.currentImageIndex = (this.currentImageIndex + direction + this.currentImageGroup.length) % this.currentImageGroup.length;
            const img = this.currentImageGroup[this.currentImageIndex];
            zoom(img, img.src, 0, 0, 0);
            this.createNavigateBtn();
        }
    }
    const getDownPathList = async () => {
        const res = await gmHttp.get("https://webapi.115.com/offine/downpath");
        return "object" == typeof res ? res.data : null;
    };
    class WangPan115TaskPlugin extends BasePlugin {
        getName() {
            return "WangPan115TaskPlugin";
        }
        getRegisterCondition() {
            return !0;
        }
        async handle() {
            $(document).on("click", ".magnet-down-115", (async event => {
                const magnet = $(event.currentTarget).data("magnet");
                let loadObj = loading();
                try {
                    await this.handleAddTask(magnet);
                } catch (e2) {
                    show.error("发生错误:" + e2);
                    console.error(e2);
                } finally {
                    loadObj.close();
                }
            }));
        }
        async handleAddTask(magnetLink) {
            let magnets = [];
            Array.isArray(magnetLink) ? magnets = magnetLink.map((m => m.trim())).filter((m => "" !== m)) : "string" == typeof magnetLink && (magnets = magnetLink.split("\n").map((m => m.trim())).filter((m => "" !== m)));
            if (0 === magnets.length) {
                show.error("未发现有效的磁力链接");
                return;
            }
            const singInfo = await (async () => {
                const res = await gmHttp.get("https://115.com/?ct=offline&ac=space&_=" + (new Date).getTime());
                return "object" == typeof res ? res : null;
            })();
            if (!singInfo) {
                show.error("未登录115网盘", {
                    close: !0,
                    duration: -1,
                    callback: () => window.open("https://115.com")
                });
                return;
            }
            const {sign: sign, time: time} = singInfo, userId = await this.getUserId();
            let result;
            const isBatch = magnets.length > 1;
            result = isBatch ? await (async (magnets, wp_path_id = "", uid, sign, time) => {
                const formData = new FormData;
                formData.append("wp_path_id", wp_path_id);
                formData.append("uid", uid);
                formData.append("sign", sign);
                formData.append("time", time);
                magnets.forEach(((url, index) => {
                    formData.append(`url[${index}]`, url);
                }));
                return await gmHttp.postFormData("https://115.com/web/lixian/?ct=lixian&ac=add_task_urls", formData);
            })(magnets, userId, "", sign, time) : await (async (magnet, wp_path_id = "", uid, sign, time) => {
                const formData = new FormData;
                formData.append("url", magnet);
                formData.append("wp_path_id", wp_path_id);
                formData.append("uid", uid);
                formData.append("sign", sign);
                formData.append("time", time);
                return await gmHttp.postFormData("https://115.com/web/lixian/?ct=lixian&ac=add_task_url", formData);
            })(magnets[0], userId, sign, time);
            console.log(isBatch ? "批量离线返回值:" : "单条离线返回值:", result);
            if (!1 === result.state) {
                show.error("添加失败: " + result.error_msg);
                return;
            }
            let openUrl = "https://115.com/?tab=offline&mode=wangpan", title = isBatch ? `成功添加 ${magnets.length} 个任务,是否前往查看?` : "添加成功, 是否前往查看?";
            if (!isBatch) {
                const infoHash = result.info_hash, fileId = await this.getFileId(userId, sign, time, infoHash);
                fileId && (openUrl = `https://115.com/?cid=${fileId}&offset=0&mode=wangpan`);
            }
            CommonUtil.q(null, title, (async () => {
                window.open(openUrl);
            }));
        }
        async getUserId() {
            let downPathList = await getDownPathList();
            if (downPathList && downPathList.length > 0) return downPathList[0].id;
            {
                show.info("没有默认离线目录, 正在创建中...");
                const dirId = (await (async (dirName, pid = 0) => {
                    const formData = new FormData;
                    formData.append("pid", pid);
                    formData.append("cname", dirName);
                    return await gmHttp.postFormData("https://webapi.115.com/files/add", formData);
                })("云下载")).file_id;
                await (async dirId => {
                    const formData = new FormData;
                    formData.append("file_id", dirId);
                    return await gmHttp.postFormData("https://webapi.115.com/offine/downpath", formData);
                })(dirId);
                show.info("创建完成, 开始执行离线下载");
                downPathList = await getDownPathList();
                if (downPathList && downPathList.length > 0) return downPathList[0].id;
                throw new Error("获取115用户Id失败");
            }
        }
        async getFileId(userId, sign, time, infoHash) {
            const taskList = await (async (uid, sign, time) => {
                const formData = new FormData;
                formData.append("page", "1");
                formData.append("uid", uid);
                formData.append("sign", sign);
                formData.append("time", time);
                return (await gmHttp.postFormData("https://115.com/web/lixian/?ct=lixian&ac=task_lists", formData)).tasks;
            })(userId, sign, time);
            let fileId = null;
            for (let i = 0; i < taskList.length; i++) {
                let task = taskList[i];
                if (task.info_hash === infoHash) {
                    fileId = task.file_id;
                    break;
                }
            }
            return fileId;
        }
    }
    const ICON_FOLD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M18 15l-6-6-6 6"/></svg>', ICON_UNFOLD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>';
    class MagnetExtractorPlugin extends BasePlugin {
        constructor() {
            super(...arguments);
            __publicField(this, "links", []);
            __publicField(this, "isCollapsed", !1);
            __publicField(this, "position", {
                top: 50,
                left: window.innerWidth - 400
            });
        }
        getName() {
            return "MagnetExtractorPlugin";
        }
        getRegisterCondition() {
            return !0;
        }
        async initCss() {
            return "\n            <style>\n                .magnet-extractor-side-panel {\n                    position: fixed;\n                    width: 380px;\n                    max-height: 85vh;\n                    background: #fff;\n                    box-shadow: 0 8px 32px rgba(0,0,0,0.2);\n                    border-radius: 12px;\n                    display: flex;\n                    flex-direction: column;\n                    z-index: 500;\n                    border: 1px solid #eee;\n                    font-family: system-ui, -apple-system, sans-serif;\n                    overscroll-behavior: contain;\n                    user-select: none;\n                }\n                \n                \n                /* 折叠状态样式 */\n                .magnet-extractor-side-panel.collapsed {\n                    max-height: 48px; /* 只保留 header 的高度 */\n                    width: 200px;\n                    overflow: hidden;\n                    padding-bottom: 0; \n                }\n\n\n                .magnet-header {\n                    padding: 12px 15px;\n                    background: #f8f9fa;\n                    border-bottom: 1px solid #eee;\n                    display: flex;\n                    justify-content: space-between;\n                    align-items: center;\n                    border-radius: 12px 12px 0 0;\n                    flex-shrink: 0;\n                    cursor: pointer; /* 点击 header 也可以折叠 */\n                }\n                .magnet-header h3 { margin: 0; font-size: 14px; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n                \n                .magnet-controls {\n                    display: flex;\n                    gap: 12px;\n                    align-items: center;\n                }\n                .magnet-toggle-btn, .magnet-close { \n                    cursor: pointer; \n                    font-size: 18px; \n                    color: #666; \n                    line-height: 1;\n                    user-select: none;\n                }\n                .magnet-toggle-btn:hover { color: #2562ff; }\n\n                .magnet-list-container {\n                    flex: 1;\n                    overflow-y: auto;\n                    padding: 10px;\n                    overscroll-behavior: contain; \n                }\n                \n                /* 折叠时隐藏主体内容 */\n                .magnet-extractor-side-panel.collapsed .magnet-list-container,\n                .magnet-extractor-side-panel.collapsed .magnet-panel-footer {\n                    display: none;\n                }\n                \n\n                /* 列表项样式保持不变... */\n                .magnet-item { padding: 12px; border-bottom: 1px solid #f5f5f5; }\n                .magnet-link-text { font-size: 12px; color: #0066cc; word-break: break-all; margin-bottom: 10px; display: block; user-select: all; }\n                .magnet-item-footer { display: flex; flex-wrap: wrap; gap: 8px; }\n                .m-btn { padding: 5px 10px; border-radius: 4px; font-size: 12px; cursor: pointer; border: 1px solid #ddd; background: #fff; transition: all 0.2s; min-width: 60px; }\n                .m-btn-copy { color: #4CAF50; border-color: #4CAF50; }\n                .m-btn-locate { color: #2196F3; border-color: #2196F3; }\n                .magnet-down-115 { background-color: #2562ff; color: #fff; border: none; }\n                .magnet-down-115:hover { background-color: #1a4cd8; }\n\n                .magnet-panel-footer { padding: 15px; border-top: 1px solid #eee; flex-shrink: 0; display: flex; flex-direction: column; gap: 10px; }\n                .copy-all-btn { width: 100%; padding: 10px; background: #4CAF50; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; }\n                .down-all-115-btn { width: 100%; padding: 10px; border-radius: 6px; cursor: pointer; font-weight: bold; }\n\n                @keyframes magnet-highlight {\n                    0% { outline: 3px solid transparent; }\n                    50% { outline: 3px solid #ffeb3b; background: #fffde7; }\n                    100% { outline: 3px solid transparent; }\n                }\n                .magnet-target-highlight { animation: magnet-highlight 2s ease-in-out; }\n                \n                \n                /* 磁力插件控制按钮通用样式 */\n                .magnet-ctrl-icon {\n                    width: 24px;\n                    height: 24px;\n                    display: flex;\n                    align-items: center;\n                    justify-content: center;\n                    border-radius: 6px;\n                    cursor: pointer;\n                    transition: all 0.2s ease;\n                    color: #666;\n                    background: transparent;\n                }\n                \n                .magnet-ctrl-icon:hover {\n                    background-color: rgba(0, 0, 0, 0.05);\n                    color: #2562ff;\n                }\n                \n                .magnet-close:hover {\n                    background-color: #ff4d4f1a;\n                    color: #ff4d4f;\n                }\n                \n                .magnet-ctrl-icon svg {\n                    width: 16px;\n                    height: 16px;\n                    stroke-width: 2;\n                }\n                \n                /* 折叠时 Header 的圆角微调 */\n                .magnet-extractor-side-panel.collapsed {\n                    max-height: 48px;\n                    width: 220px;\n                    border-radius: 12px;\n                }\n            </style>\n        ";
        }
        async handle() {
            const cachedStatus = await cacheManager.getItem(cacheManager.magnetExtractorCollapsed_key);
            this.isCollapsed = !0 === cachedStatus;
            const cachedPos = await cacheManager.getItem("magnet_extractor_pos_key"), winW = window.innerWidth, winH = window.innerHeight;
            if (cachedPos) {
                const safeLeft = Math.max(0, Math.min(cachedPos.left, winW - 50)), safeTop = Math.max(0, Math.min(cachedPos.top, winH - 50));
                this.position = {
                    top: safeTop,
                    left: safeLeft
                };
            } else this.position = {
                top: 50,
                left: winW - 400
            };
            this.startExtractor(!0);
        }
        startExtractor(isSilent = !1) {
            this.extractLinks();
            0 !== this.links.length ? this.renderPanel() : isSilent || show.info("本页未发现磁力或 ed2k 链接");
        }
        extractLinks() {
            const magnetRegex = /magnet:\?xt=urn:btih:[a-zA-Z0-9]{32,40}/gi, ed2kRegex = /ed2k:\/\/\|file\|[^|]+\|\d+\|[a-fA-F0-9]{32}\|/gi, foundMap = new Map;
            $("a").each(((i, el) => {
                const href = $(el).attr("href") || "";
                (href.match(magnetRegex) || href.match(ed2kRegex)) && (foundMap.has(href) || foundMap.set(href, el));
            }));
            [ "div.blockcode", "pre", "code", "td.t_f", "div.pcb" ].forEach((selector => {
                $(selector).each(((i, box) => {
                    const fullText = box.innerText;
                    [ magnetRegex, ed2kRegex ].forEach((reg => {
                        const matches = fullText.match(reg);
                        matches && matches.forEach((m => {
                            const url = m.replace(/[\r\n]/g, "").trim();
                            foundMap.has(url) || foundMap.set(url, box);
                        }));
                    }));
                }));
            }));
            const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, !1);
            let node;
            for (;node = walker.nextNode(); ) {
                const text = node.nodeValue;
                [ magnetRegex, ed2kRegex ].forEach((reg => {
                    const matches = text.match(reg);
                    matches && matches.forEach((m => {
                        const url = m.replace(/[\r\n]/g, "").trim();
                        foundMap.has(url) || foundMap.set(url, node.parentElement);
                    }));
                }));
            }
            this.links = Array.from(foundMap.entries()).map((([url, el]) => ({
                url: url,
                element: el
            })));
        }
        renderPanel() {
            $("#magnet-extractor-side").length && $("#magnet-extractor-side").remove();
            const allLinksCombined = this.links.map((m => m.url)).join("\n"), html = `\n            <div id="magnet-extractor-side" class="magnet-extractor-side-panel ${this.isCollapsed ? "collapsed" : ""}"\n                style="top: ${this.position.top}px; left: ${this.position.left}px;">\n                <div class="magnet-header" style="cursor: move;">\n                    <h3>🧲 已提取 (${this.links.length})</h3>\n                    <div class="magnet-controls">\n                        <div class="magnet-ctrl-icon magnet-toggle-btn" title="折叠/展开">\n                            ${this.isCollapsed ? ICON_UNFOLD : ICON_FOLD}\n                        </div>\n                        <div class="magnet-ctrl-icon magnet-close" title="关闭">\n                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>\n                        </div>\n                    </div>\n                </div>\n                <div class="magnet-list-container">\n                    ${this.links.map(((item, index) => `\n                        <div class="magnet-item">\n                            <span class="magnet-link-text">${item.url}</span>\n                            <div class="magnet-item-footer">\n                                <button class="m-btn m-btn-locate" data-index="${index}">定位</button>\n                                <button class="m-btn m-btn-copy" data-index="${index}">复制</button>\n                                <button class="m-btn magnet-down-115" data-magnet="${item.url}">115离线</button>\n                            </div>\n                        </div>\n                    `)).join("")}\n                </div>\n                <div class="magnet-panel-footer">\n                    <button class="down-all-115-btn magnet-down-115" data-magnet="${allLinksCombined}">一键离线所有</button>\n                    <button class="copy-all-btn">复制所有链接</button>\n                    <button class="m-btn go-115-btn" style="width: 100%; padding: 8px; margin-top: 5px; color: #2562ff; border-color: #2562ff;">前往 115 网页版</button>\n                </div>\n            </div>\n        `;
            $("body").append(html);
            this.initPanelEvents();
            this.initDragEvents();
        }
        initPanelEvents() {
            const $panel = $("#magnet-extractor-side"), toggleFold = () => {
                this.isCollapsed = !this.isCollapsed;
                cacheManager.setItem(cacheManager.magnetExtractorCollapsed_key, this.isCollapsed);
                $panel.toggleClass("collapsed", this.isCollapsed);
                $panel.find(".magnet-toggle-btn").html(this.isCollapsed ? ICON_UNFOLD : ICON_FOLD);
            };
            $panel.find(".magnet-toggle-btn").on("click", (e2 => {
                e2.stopPropagation();
                toggleFold();
            }));
            $panel.on("click", ".go-115-btn", (() => {
                window.open("https://115.com/?cid=0&offset=0&mode=wangpan", "_blank");
            }));
            $panel.find(".magnet-close").on("click", (e2 => {
                e2.stopPropagation();
                $panel.remove();
            }));
            $panel.on("click", ".m-btn-locate", (e2 => {
                const idx = $(e2.currentTarget).data("index"), target = this.links[idx].element;
                if (target) {
                    target.scrollIntoView({
                        behavior: "smooth",
                        block: "center"
                    });
                    $(target).addClass("magnet-target-highlight");
                    setTimeout((() => $(target).removeClass("magnet-target-highlight")), 2e3);
                }
            }));
            $panel.on("click", ".m-btn-copy", (e2 => {
                const $btn = $(e2.currentTarget);
                this.copyToClipboard(this.links[$btn.data("index")].url);
                const originalText = $btn.text();
                $btn.text("已复制 ✅");
                setTimeout((() => $btn.text(originalText)), 1500);
            }));
            $panel.on("click", ".copy-all-btn", (e2 => {
                const $btn = $(e2.currentTarget);
                this.copyToClipboard(this.links.map((m => m.url)).join("\n"));
                const originalText = $btn.text();
                $btn.text("全部复制成功! ✅");
                setTimeout((() => $btn.text(originalText)), 1500);
            }));
        }
        initDragEvents() {
            const $panel = $("#magnet-extractor-side"), $header = $panel.find(".magnet-header");
            let isDragging = !1, offset = {
                x: 0,
                y: 0
            };
            $header.on("mousedown", (e2 => {
                if ($(e2.target).closest(".magnet-controls").length) return;
                isDragging = !0;
                const rect = $panel[0].getBoundingClientRect();
                offset = {
                    x: e2.clientX - rect.left,
                    y: e2.clientY - rect.top
                };
                $(document).on("mousemove.magnet_drag", (de => {
                    if (!isDragging) return;
                    let newLeft = de.clientX - offset.x, newTop = de.clientY - offset.y;
                    newLeft = Math.max(0, Math.min(window.innerWidth - $panel.outerWidth(), newLeft));
                    newTop = Math.max(0, Math.min(window.innerHeight - $panel.outerHeight(), newTop));
                    $panel.css({
                        left: newLeft,
                        top: newTop
                    });
                    this.position = {
                        top: newTop,
                        left: newLeft
                    };
                }));
                $(document).on("mouseup.magnet_drag", (() => {
                    if (isDragging) {
                        isDragging = !1;
                        $(document).off(".magnet_drag");
                        cacheManager.setItem("magnet_extractor_pos_key", this.position);
                    }
                }));
            }));
        }
        copyToClipboard(text) {
            const input = document.createElement("textarea");
            input.value = text;
            document.body.appendChild(input);
            input.select();
            document.execCommand("copy");
            document.body.removeChild(input);
        }
    }
    !async function() {
        DomUtil.importResource("https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.css");
        DomUtil.importResource("https://cdn.jsdelivr.net/npm/[email protected]/dist/viewer.min.css");
        const pluginManager = new PluginManager;
        pluginManager.register(MenuPlugin);
        pluginManager.register(ImageRecognitionPlugin);
        pluginManager.register(WangPan115TaskPlugin);
        pluginManager.register(MagnetHubPlugin);
        pluginManager.register(MagnetExtractorPlugin);
        pluginManager.register(SeHuaTangPlugin);
        pluginManager.processCss().then();
        pluginManager.processPlugins().then();
    }();
}();