默认Q键激活菜单; 支持以图识图、磁力聚合、页面磁力提取...
// ==UserScript== // @name JAV-FORUM // @description 默认Q键激活菜单; 支持以图识图、磁力聚合、页面磁力提取... // @version 0.1.2 // @author JAV-FORUM // @namespace JAV-FORUM // @license MIT // @icon https://cdn-icons-png.flaticon.com/512/6576/6576105.png // @include *://*/* // @exclude *://*gemini.google.com/* // @exclude *://*googletagmanager.com/* // @exclude *://*ogs.google.com/* // @exclude *://*javdb*.com/* // @exclude *://*javbus.com/*-* // @exclude *://*javbus.com/star/* // @exclude *://*javbus.com/uncensored* // @exclude *://*javbus.com/page/* // @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 // @connect whatslink.info // @connect u9a9.com // @connect sukebei.nyaa.si // @connect btsow.lol // @connect btdig.com // @connect cld139.buzz // @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 (e) { let errorString = String(e); 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 e; runCount++; if (runCount === tryCount) { errorString.length > 200 ? console.debug(`[重试] 达到最大重试次数 (${tryCount}),最终失败`) : console.debug(`[重试] 达到最大重试次数 (${tryCount}),最终失败:`, e); throw e; } 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 (e) { 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 (e) { 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 (e) { reject(e); } }, onerror: error => { console.error("网络错误:", url); reject(new Error(error.error || "网络错误")); }, ontimeout: () => { reject(new Error("请求超时: " + url)); } }); }))), httpRetryCount); } }; !async function() { document.head.insertAdjacentHTML("beforeend", "\n <style>\n html {\n scrollbar-gutter: stable; /* 无论是否溢出,都为滚动条预留位置 */\n }\n .layui-layer-shade, .layui-layer {\n position: fixed; pointer-events: auto;\n }\n .layui-layer-shade {\n top: 0; left: 0; width: 100%; height: 100%;\n }\n \n .layui-layer {\n top: 150px;\n left: 0;\n margin: 0;\n padding: 0;\n background-color: #fff;\n border-radius: 6px !important;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n border: 1px solid #ddd;\n }\n .layui-layer-content {\n position: relative;\n font-size: 14px;\n color: #444;\n }\n .layui-layer-border {\n border: 1px solid #B2B2B2;\n box-shadow: 1px 1px 5px rgba(0, 0, 0, .2);\n }\n \n .layui-layer-load {\n background: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 80'%3E%3Cstyle%3Erect%7Banimation:m 1.8s ease-in-out infinite}@keyframes m%7B0%25,100%25%7Btransform:translateX(20px)%7D50%25%7Btransform:translateX(-20px)%7D%3C/style%3E%3Crect x='40' y='14' width='40' height='12' rx='6' fill='%23ff758c'/%3E%3Crect x='30' y='34' width='60' height='12' rx='6' fill='%234facfe' style='animation-delay:-0.45s'/%3E%3Crect x='42' y='54' width='35' height='12' rx='6' fill='%23ff9a9e' style='animation-delay:-0.9s'/%3E%3C/svg%3E\") no-repeat center !important;\n background-size: 120px auto !important;\n }\n \n .layui-layer-move {\n display: none;\n position: fixed;\n left: 0;\n top: 0;\n width: 100%;\n height: 100%;\n cursor: move;\n opacity: 0;\n filter: alpha(opacity=0);\n background-color: #fff;\n z-index: 2147483647;\n }\n \n \n /* 动画 */\n .layer-anim {\n animation-fill-mode: both;\n animation-duration: .3s;\n }\n .layer-anim-0 {\n animation-name: layer-bounceIn\n }\n @keyframes layer-bounceIn {\n 0% {\n opacity: 0;\n transform: scale(.5)\n }\n 100% {\n opacity: 1;\n transform: scale(1)\n }\n }\n .layer-anim-1 {\n animation-name: layer-fadeIn\n }\n @keyframes layer-fadeIn {\n 0% {\n opacity: 0\n }\n 100% {\n opacity: 1\n }\n }\n \n /* 标题栏 */\n .layui-layer-title {\n height: 40px;\n line-height: 40px;\n padding: 0 80px 0 20px;\n background-color: #f2f2f2;\n color: #333;\n font-size: 14px;\n font-weight: 500;\n border-bottom: 1px solid #eee;\n border-radius: 6px 6px 0 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n \n .layui-layer-setwin {\n position: absolute;\n right: 15px;\n top: 10px;\n font-size: 0;\n line-height: initial;\n }\n .layui-layer-setwin a, \n .layui-layer-btn a {\n display: inline-block;\n *display: inline;\n *zoom: 1;\n vertical-align: top;\n }\n .layui-layer-setwin .layui-layer-close {\n position: relative;\n width: 20px;\n height: 20px;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M557.3 512l213.4-213.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L512 466.7 298.7 253.3c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L466.7 512 253.3 725.3c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L512 557.3l213.4 213.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L557.3 512z' fill='%23000' fill-opacity='0.5'/%3E%3C/svg%3E\") !important;\n cursor: pointer;\n }\n .layui-layer-setwin .layui-layer-close:hover {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M557.3 512l213.4-213.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L512 466.7 298.7 253.3c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L466.7 512 253.3 725.3c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L512 557.3l213.4 213.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L557.3 512z' fill='%231E9FFF'/%3E%3C/svg%3E\") !important;\n }\n \n /* 按钮栏 */\n .layui-layer-btn {\n text-align: right;\n padding: 0 15px 12px;\n pointer-events: auto;\n user-select: none;\n }\n .layui-layer-btn a {\n height: 28px;\n line-height: 28px;\n margin: 5px 5px 0;\n padding: 0 15px;\n border: 1px solid #dedede;\n background-color: #fff;\n color: #333;\n border-radius: 2px;\n font-weight: 400;\n cursor: pointer;\n text-decoration: none;\n }\n .layui-layer-btn a:hover {\n opacity: 0.9;\n text-decoration: none;\n }\n .layui-layer-btn a:active {\n opacity: 0.8;\n }\n .layui-layer-btn .layui-layer-btn0 {\n background-color: #1E9FFF;\n color: #fff;\n border: none;\n border-radius: 3px;\n }\n .layui-layer-btn .layui-layer-btn0:hover {\n background-color: #0081cc;\n }\n .layui-layer-btn .layui-layer-btn1 { \n background-color: #fff;\n color: #666;\n border: 1px solid #ccc;\n border-radius: 3px;\n margin-right: 10px;\n }\n .layui-layer-btn .layui-layer-btn1:hover {\n background-color: #f5f5f5;\n color: #333;\n }\n .multi-confirm .layui-layer-btn .layui-layer-btn0 { \n background-color: #fff;\n color: #666;\n border: 1px solid #ccc;\n border-radius: 3px;\n margin-right: 10px;\n }\n .multi-confirm .layui-layer-btn .layui-layer-btn0:hover {\n background-color: #f5f5f5;\n color: #333;\n }\n .layui-layer-btn .layui-layer-btn2 { \n background-color: #fff;\n color: #666;\n border: 1px solid #ccc;\n border-radius: 3px;\n margin-right: 10px;\n }\n .layui-layer-btn .layui-layer-btn2:hover {\n background-color: #f5f5f5;\n color: #333;\n }\n \n /* 定制化 */\n .layui-layer-confirm {\n min-width: 300px;\n }\n .layui-layer-confirm .layui-layer-content {\n position: relative;\n padding: 20px;\n line-height: 24px;\n word-break: break-all;\n font-size: 14px;\n overflow-x: hidden;\n overflow-y: auto;\n }\n \n .layui-layer-html .layui-layer-content {\n position: relative;\n overflow: auto;\n }\n .layui-layer-html .layui-layer-btn, .layui-layer-iframe .layui-layer-btn {\n padding-top: 10px;\n }\n \n \n .layui-layer-iframe iframe {\n display: block;\n width: 100%;\n border: none;\n background: transparent;\n }\n \n /* 关闭动画 */\n .layer-anim-close {\n animation-name: layer-bounceOut;\n animation-fill-mode: both;\n animation-duration: .2s;\n }\n @keyframes layer-bounceOut {\n 100% {\n opacity: 0;\n transform: scale(.7);\n }\n 30% {\n transform: scale(1.05);\n }\n 0% {\n transform: scale(1);\n }\n }\n \n @media screen and (max-width: 1100px) {\n .layui-layer-iframe {\n overflow-y: auto;\n }\n }\n \n /* 图片查看器核心容器 */\n .layui-layer-photos {\n background: none !important;\n box-shadow: none !important;\n border: none !important;\n }\n .viewer-main {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n position: relative;\n cursor: default;\n user-select: none;\n }\n #viewer-img {\n position: absolute;\n cursor: grab;\n touch-action: none;\n max-width: none;\n max-height: none;\n }\n .viewer-transitioning {\n transition: all 0.3s ease;\n }\n /* 底部缩略图栏 */\n .viewer-bottom-bar {\n position: fixed;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 10px;\n padding: 10px;\n background: rgba(0, 0, 0, 0.5);\n border-radius: 8px;\n z-index: 100000000;\n max-width: 90%;\n overflow-x: auto;\n }\n \n /* 缩略图滚动条美化 */\n .viewer-bottom-bar::-webkit-scrollbar { height: 6px; }\n .viewer-bottom-bar::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 10px; }\n .viewer-bottom-bar::-webkit-scrollbar-track { background: transparent; }\n \n /* 缩略图单项 */\n .thumb-item {\n width: 50px;\n height: 50px;\n cursor: pointer;\n border: 2px solid transparent;\n border-radius: 4px;\n overflow: hidden;\n flex-shrink: 0;\n }\n .thumb-item img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n opacity: 0.6;\n }\n .thumb-item.active { border-color: #1E9FFF; }\n .thumb-item.active img, .thumb-item:hover img { opacity: 1; }\n \n /* 工具栏 */\n .viewer-tools {\n position: fixed;\n bottom: 70px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(0, 0, 0, 0.7);\n padding: 6px 20px;\n border-radius: 25px;\n color: #fff;\n display: flex;\n gap: 15px;\n z-index: 100000001;\n align-items: center;\n }\n .viewer-tools i {\n cursor: pointer;\n font-style: normal;\n font-weight: bold;\n font-size: 18px;\n width: 32px;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.2s;\n }\n .viewer-tools i:hover {\n background: rgba(255, 255, 255, 0.2);\n border-radius: 50%;\n color: #1E9FFF;\n }\n \n /* 1:1 按钮特殊样式 */\n .reset-1-1 {\n background: #444;\n padding: 0 10px !important;\n border-radius: 4px;\n font-size: 12px !important;\n height: 24px !important;\n width: auto !important;\n }\n \n .viewer-counter { font-size: 13px; color: #ccc; margin-left: 5px; }\n\n /* Loading 动画容器 */\n .viewer-loading {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: none; /* 默认隐藏 */\n flex-direction: column;\n align-items: center;\n color: #fff;\n z-index: 100000002;\n }\n \n /* 转圈动画 */\n .loader-spinner {\n width: 40px;\n height: 40px;\n border: 3px solid rgba(255, 255, 255, 0.3);\n border-radius: 50%;\n border-top-color: #1E9FFF;\n animation: viewer-spin 1s ease-in-out infinite;\n margin-bottom: 10px;\n }\n \n @keyframes viewer-spin {\n to { transform: rotate(360deg); }\n }\n </style>\n "); const layerEscManager = { layerIndexStack: [], initialized: !1, init() { if (this.initialized) return; $(document).on("keydown.layerEsc", (e => this.handleKey(e))); window.addEventListener("message", (event => { event.data && "ESC_LAYER" === event.data.type && this.handleKey({ key: "Escape" }); })); this.initialized = !0; }, handleKey(e) { if ("Escape" !== e.key && 27 !== e.keyCode) return; if (0 === this.layerIndexStack.length) return; if (this.layerIndexStack.length > 0) { const topLayerIndex2 = this.layerIndexStack[this.layerIndexStack.length - 1]; try { const $iframe = $(`#layui-layer-iframe${topLayerIndex2}`); let hasNestedLayer = !1; if ($iframe.length > 0) { const iframeDoc = $iframe[0].contentDocument || $iframe[0].contentWindow.document; hasNestedLayer = $(iframeDoc).find(".layui-layer").length > 0; } else { hasNestedLayer = $(`#layui-layer${topLayerIndex2}`).find(".layui-layer").length > 0; } if (hasNestedLayer) return; } catch (err) { console.warn("[EscManager] 无法探测子窗口 (跨域限制)"); } } const topLayerIndex = this.layerIndexStack.pop(); layer2.close(topLayerIndex); }, push(layerIndex) { var _a; this.init(); -1 === this.layerIndexStack.indexOf(layerIndex) && this.layerIndexStack.push(layerIndex); try { const $iframe = $(`#layui-layer-iframe${layerIndex}`), eventNamespace = "keydown.layerEscProxy", iframeDocument = null == (_a = $iframe[0]) ? void 0 : _a.contentDocument; iframeDocument && $(iframeDocument).off(eventNamespace).on(eventNamespace, (e => this.handleKey(e))); } catch (e) { console.error("[EscManager] Iframe 内部监听绑定失败 (可能是跨域):", e); } } }, layer2 = { typeMap: { CONFIRM: "confirm", HTML: "html", IFRAME: "iframe", VIEWER: "viewer" }, index: 0, btn: [ "确认", "取消" ], layerEscManager: layerEscManager, endFunMaps: {} }; "undefined" != typeof unsafeWindow && (unsafeWindow.layer = layer2); window.layer = layer2; layer2.open = options => { const originalSuccess = options.success; options.success = function(layero, index) { "function" == typeof originalSuccess && originalSuccess.call(this, layero, index); layerEscManager.push(index); }; return new Dialog(options).index; }; layer2.confirm = (content, options, yes, cancel) => { "function" == typeof options && ([cancel, yes, options] = [ yes, options, {} ]); return layer2.open({ content: content, type: layer2.typeMap.CONFIRM, yes: yes, btn2: cancel, ...options }); }; layer2.close = (index, callback) => { const $layero = $(`#layui-layer${index}`); if (!$layero[0]) return; const remove = () => { var _a, _b; $(window).off(`resize.layer${index}`); $layero.remove(); const openLayerCount = document.querySelectorAll(".layui-layer-shade").length; document.documentElement.style.overflow = openLayerCount > 0 ? "hidden" : ""; null == (_b = (_a = layer2.endFunMaps)[index]) || _b.call(_a); delete layer2.endFunMaps[index]; null == callback || callback(); }; $(`#layui-layer-shade${index}`).remove(); if ($layero.data("isOutAnim")) { const closeAnim = "layer-anim-close"; $layero.addClass(`layer-anim ${closeAnim}`).one("animationend", remove); setTimeout(remove, 200); } else remove(); layerEscManager.layerIndexStack = layerEscManager.layerIndexStack.filter((i => i !== index)); }; layer2.closeAll = (type, callback) => { if ("function" == typeof type) { callback = type; type = null; } const $layers = $(".layui-layer"), total = $layers.length; if (0 === total) return "function" == typeof callback && callback(); $layers.each(((index, el) => { const $el = $(el), layerType = $el.attr("data-type"), layerIndex = $el.attr("data-index"); if (!type || layerType === type) { const isLast = index === total - 1; layer2.close(layerIndex, isLast ? callback : null); } })); }; class Dialog { constructor(settings) { this.index = ++layer2.index; this.config = { type: layer2.typeMap.CONFIRM, shade: .3, fixed: !0, title: "信息", offset: "auto", area: "auto", zIndex: 19891014, maxWidth: $(window).width() - 30, anim: 0, isOutAnim: !0, scrollbar: !0, moveOut: !1, moveEnd: void 0, ...settings }; const legacyTypes = [ layer2.typeMap.CONFIRM, layer2.typeMap.HTML, layer2.typeMap.IFRAME ]; "number" == typeof this.config.type && legacyTypes[this.config.type] && (this.config.type = legacyTypes[this.config.type]); if (!Object.values(layer2.typeMap).includes(this.config.type)) { layer2.confirm(`[Layer Error]: 无效的弹窗类型 "${this.config.type}"`); throw new Error(`[Layer Error]: 无效的弹窗类型 "${this.config.type}"`); } this.create(); } create() { const {config: config, index: index} = this, {content: content, type: type, shade: shade, id: id, area: area, anim: anim, isOutAnim: isOutAnim, fixed: fixed} = config, $body = $("body"); if (id && $(`#${id}`)[0]) return; "string" == typeof area ? config.area = "auto" === area ? [ "", "" ] : [ area, "" ] : null == area && (config.area = [ "", "" ]); switch (type) { case layer2.typeMap.CONFIRM: config.btn = "btn" in config ? config.btn : "确认"; layer2.closeAll("confirm"); break; case layer2.typeMap.HTML: break; case layer2.typeMap.IFRAME: config.content = `\n <iframe id="layui-layer-iframe${index}" name="layui-layer-iframe${index}" \n src="${content}" class="layui-layer-load" onload="this.className='';"></iframe>`; } const {shadeHtml: shadeHtml, mainHtml: mainHtml} = this.renderHtml(); $body.append(shadeHtml); $body.append(mainHtml); this.layero = $(`#layui-layer${index}`); this.shadeo = $(`#layui-layer-shade${index}`); config.scrollbar || (document.documentElement.style.overflow = "hidden"); this.auto(index); const [opacity, color] = Array.isArray(shade) ? shade : [ shade, "#000" ]; this.shadeo.css({ "background-color": color || "#000", opacity: opacity ?? 0 }); this.offset(); fixed && $(window).on(`resize.layer${index}`, (() => { this.offset(); config.area.some((val => /^\d+%$/.test(val))) && this.auto(index); })); this.move().callback(); if (-1 !== anim) { const animClass = `layer-anim layer-anim-${anim}`; this.layero.addClass(animClass).one("animationend", (function() { $(this).removeClass(animClass); })); } isOutAnim && this.layero.data("isOutAnim", !0); } renderHtml() { const currentIndex = this.index, config = this.config, zIndex = config.zIndex + currentIndex; config.zIndex = zIndex; const shadeHtml = config.shade ? `\n <div class="layui-layer-shade" id="layui-layer-shade${currentIndex}" data-index="${currentIndex}" \n style="z-index:${zIndex - 1}; background-color:${config.shade[1] || "#000"}; opacity:${config.shade[0] || config.shade};">\n </div>` : "", classes = [ "layui-layer", `layui-layer-${config.type}`, config.type !== layer2.typeMap.IFRAME || config.shade ? "" : "layui-layer-border", config.skin ].filter(Boolean).join(" "), titleHtml = config.title ? `<div class="layui-layer-title" >${config.title}</div>` : "", area = config.area || [ "auto", "auto" ], closeBtnHtml = config.title ? '<span class="layui-layer-setwin"> <a class="layui-layer-close"></a></span>' : ""; return { shadeHtml: shadeHtml, mainHtml: `\n <div class="${classes}" \n id="layui-layer${currentIndex}" data-type="${config.type}" data-index="${currentIndex}" \n style="z-index:${zIndex}; width:${area[0]}; height:${area[1]}; position:${config.fixed ? "fixed" : "absolute"};">\n \n ${titleHtml}\n \n <div id="${config.id || ""}" class="layui-layer-content">\n ${config.content}\n </div>\n \n ${closeBtnHtml}\n \n ${config.btn ? `\n <div class="layui-layer-btn">\n ${("string" == typeof config.btn ? [ config.btn ] : config.btn).map(((text, i) => `<a class="layui-layer-btn${i}">${text}</a>`)).join("")}\n </div>` : ""}\n </div>\n ` }; } auto(index) { const {config: config} = this, $layero = $(`#layui-layer${index}`), $win = $(window); "" === config.area[0] && config.maxWidth > 0 && $layero.outerWidth() > config.maxWidth && $layero.width(config.maxWidth); const area = [ $layero.innerWidth(), $layero.innerHeight() ], titHeight = $layero.find(".layui-layer-title").outerHeight() || 0, btnHeight = $layero.find(".layui-layer-btn").outerHeight() || 0, setContentHeight = selector => { const $elem = $layero.find(selector), paddingTop = parseFloat($elem.css("padding-top")) || 0, paddingBottom = parseFloat($elem.css("padding-bottom")) || 0; $elem.height(area[1] - titHeight - btnHeight - (paddingTop + paddingBottom)); }; if (config.type === layer2.typeMap.IFRAME) setContentHeight("iframe"); else if ("" === config.area[1]) { const currentHeight = $layero.outerHeight(), maxHeight = config.maxHeight > 0 ? config.maxHeight : $win.height(); if (currentHeight > maxHeight) { area[1] = maxHeight; setContentHeight(".layui-layer-content"); } } else setContentHeight(".layui-layer-content"); return this; } offset() { const {config: config, layero: layero} = this, $win = $(window), area = [ layero.outerWidth(), layero.outerHeight() ], winSize = [ window.innerWidth || document.documentElement.clientWidth, window.innerHeight || document.documentElement.clientHeight ]; let top = (winSize[1] - area[1]) / 2, left = (winSize[0] - area[0]) / 2; const offsetMap = { t: [ 0, left ], r: [ top, winSize[0] - area[0] ], b: [ winSize[1] - area[1], left ], l: [ top, 0 ], lt: [ 0, 0 ], lb: [ winSize[1] - area[1], 0 ], rt: [ 0, winSize[0] - area[0] ], rb: [ winSize[1] - area[1], winSize[0] - area[0] ] }; Array.isArray(config.offset) ? [top, left = left] = config.offset : "auto" !== config.offset && (offsetMap[config.offset] ? [top, left] = offsetMap[config.offset] : top = config.offset); const parsePos = (pos, sizeIndex) => "string" == typeof pos && /%$/.test(pos) ? winSize[sizeIndex] * parseFloat(pos) / 100 : parseFloat(pos); if (!config.fixed) { top = parsePos(top, 1) + $win.scrollTop(); left = parsePos(left, 0) + $win.scrollLeft(); } layero.css({ top: top, left: left }); } move() { const {config: config, layero: layero} = this, $moveElem = layero.find(".layui-layer-title"); if (!$moveElem[0]) return this; const dict = {}; $moveElem.css("cursor", "move"); $moveElem.on("mousedown", (e => { e.preventDefault(); dict.moveStart = !0; dict.offset = [ e.clientX - parseFloat(layero.css("left")), e.clientY - parseFloat(layero.css("top")) ]; const mouseMoveHandle = e2 => { if (!dict.moveStart) return; e2.preventDefault(); const isFixed = "fixed" === layero.css("position"), stX = isFixed ? 0 : $(window).scrollLeft(), stY = isFixed ? 0 : $(window).scrollTop(); let X = e2.clientX - dict.offset[0], Y = e2.clientY - dict.offset[1]; if (!config.moveOut) { const maxLeft = $(window).width() - layero.outerWidth() + stX, maxTop = $(window).height() - layero.outerHeight() + stY; X = Math.max(stX, Math.min(X, maxLeft)); Y = Math.max(stY, Math.min(Y, maxTop)); } layero.css({ left: X, top: Y }); }, mouseUpHandle = () => { var _a; dict.moveStart = !1; $(document).off("mousemove", mouseMoveHandle).off("mouseup", mouseUpHandle); null == (_a = config.moveEnd) || _a.call(config, layero); }; $(document).on("mousemove", mouseMoveHandle).on("mouseup", mouseUpHandle); })); return this; } callback() { const {config: config, index: index, layero: layero, shadeo: shadeo} = this; config.success && (config.type === layer2.typeMap.IFRAME ? layero.find("iframe").on("load", (() => config.success(layero, index))) : config.success(layero, index)); layero.find(".layui-layer-btn a").on("click", (function() { const btnIndex = $(this).index(); if (0 === btnIndex) { const firstBtnCallback = config.yes || config.btn1; firstBtnCallback ? firstBtnCallback(index, layero) : layer2.close(index); } else { const callback = config[`btn${btnIndex + 1}`]; !1 !== (null == callback ? void 0 : callback(index, layero)) && layer2.close(index); } })); layero.find(".layui-layer-close").on("click", (() => { var _a; !1 !== (null == (_a = config.cancel) ? void 0 : _a.call(config, index, layero)) && layer2.close(index); })); config.shadeClose && shadeo.on("click", (() => layer2.close(index))); config.end && (layer2.endFunMaps[index] = config.end); } } class PhotoViewer { constructor(urls, config = {}) { this.urls = Array.isArray(urls) ? urls : [ urls ]; this.settings = Object.assign({ startIndex: 0, toTop: !0, initZoom: !0, defaultScale: 1.4, minScale: .1, zoomStep: .1, topPadding: 20, scrollSpeed: .8 }, config); this.state = { currentIndex: this.settings.startIndex, rotate: 0, scale: 1, isDragging: !1, startPos: { x: 0, y: 0 }, currentPos: { top: 0, left: 0 }, ticking: !1 }; this.layerIdx = null; this.$layero = null; this.$img = null; this.$container = null; this.init(); } _getZoomKey(imgSrc) { try { return `viewer_zoom_${new URL(imgSrc).host}`; } catch (e) { return null; } } _getInitialScale(url) { if (!this.settings.initZoom) return 1; const saved = localStorage.getItem(this._getZoomKey(url)); return saved ? parseFloat(saved) : this.settings.defaultScale; } _saveScale() { const {scale: scale} = this.state; if (scale <= 0) return; const src = this.$img.attr("src"), cacheKey = this._getZoomKey(src); cacheKey && localStorage.setItem(cacheKey, scale.toFixed(2)); } render() { const {currentIndex: currentIndex, scale: scale} = this.state; return `\n <div class="viewer-main">\n <div class="viewer-loading">\n <div class="loader-spinner"></div>\n <span>加载中...</span>\n </div>\n \n <img src="${this.urls[currentIndex]}" id="viewer-img" class="viewer-transitioning" referrerPolicy="no-referrer" alt="">\n \n <div class="viewer-tools">\n <span class="zoom-percent">${Math.round(100 * scale)}%</span>\n \n <i class="zoom-out" data-action="zoomOut">-</i>\n <i class="zoom-in" data-action="zoomIn">+</i>\n <i class="reset-1-1" data-action="reset">1:1</i>\n <i class="rotate-btn" data-action="rotate">⟳</i>\n <span class="viewer-counter">${currentIndex + 1} / ${this.urls.length}</span>\n </div>\n \n <div class="viewer-bottom-bar">\n ${this.urls.map(((url, i) => `\n <div class="thumb-item ${i === currentIndex ? "active" : ""}" data-index="${i}">\n <img src="${url}" referrerPolicy="no-referrer" alt="">\n </div>\n `)).join("")}\n </div>\n </div>\n `; } updateImgStyle() { const {scale: scale, rotate: rotate, currentPos: currentPos} = this.state, img = this.$img[0], targetW = img.naturalWidth * scale, targetH = img.naturalHeight * scale; this.$img.css({ width: `${targetW}px`, height: `${targetH}px`, top: `${currentPos.top}px`, left: `${currentPos.left}px`, transform: `rotate(${rotate}deg)` }); this.$layero.find(".zoom-percent").text(`${Math.round(100 * scale)}%`); this._saveScale(); } _calculatePosition(mode = "center") { const img = this.$img[0], targetW = img.naturalWidth * this.state.scale, targetH = img.naturalHeight * this.state.scale, left = (this.$container.width() - targetW) / 2; let top = (this.$container.height() - targetH) / 2; ("top" === mode || "auto" === mode && this.settings.toTop) && (top = this.settings.topPadding); return { left: left, top: top }; } _resetState(index = this.state.currentIndex) { this.state.currentIndex = index; this.state.rotate = 0; this.state.scale = this._getInitialScale(this.urls[index]); } handleZoom(isZoomIn, centerX = void 0, centerY = void 0) { const oldScale = this.state.scale, zoomStep = this.settings.zoomStep, newScale = isZoomIn ? oldScale + zoomStep : Math.max(this.settings.minScale, oldScale - zoomStep); if (newScale === oldScale) return; const cX = void 0 !== centerX ? centerX : this.$container.width() / 2, cY = void 0 !== centerY ? centerY : this.$container.height() / 2, ratio = newScale / oldScale; this.state.currentPos.left = cX - (cX - this.state.currentPos.left) * ratio; this.state.currentPos.top = cY - (cY - this.state.currentPos.top) * ratio; this.state.scale = newScale; this.updateImgStyle(); } switchImage(index) { if (index !== this.state.currentIndex) { this._resetState(index); this.$layero.find(".thumb-item").removeClass("active").find("img").css("opacity", "0.6"); this.$layero.find(`.thumb-item[data-index="${index}"]`).addClass("active").find("img").css("opacity", "1"); this.$layero.find(".viewer-counter").text(`${index + 1} / ${this.urls.length}`); this.$img.attr("src", this.urls[index]); this.onImageReady((() => { this.state.currentPos = this._calculatePosition("auto"); this.updateImgStyle(); })); } } onImageReady(callback) { const $loader = this.$layero.find(".viewer-loading"); $loader.show(); this.$img.css("opacity", "0"); const done = () => { $loader.hide(); this.$img.css("opacity", "1"); callback(); }; if (this.$img[0].complete && this.$img[0].naturalWidth > 0) return done(); this.$img.one("load", done).one("error", (() => { $loader.hide(); console.error("图片加载失败"); })); } bindEvents() { this.$img.on("mousedown", (e => { if (0 === e.button) { e.preventDefault(); this.state.isDragging = !0; this.$img.removeClass("viewer-transitioning"); this.state.startPos = { x: e.clientX - this.state.currentPos.left, y: e.clientY - this.state.currentPos.top }; this.$img.css({ cursor: "grabbing" }); } })); $(document).on("mousemove.PhotoViewer", (e => { if (this.state.isDragging) { this.state.currentPos.left = e.clientX - this.state.startPos.x; this.state.currentPos.top = e.clientY - this.state.startPos.y; this.updateImgStyle(); } })).on("mouseup.PhotoViewer", (() => { if (this.state.isDragging) { this.state.isDragging = !1; this.$img.css({ cursor: "grab" }); this.$img.addClass("viewer-transitioning"); } })); this.$layero.on("wheel", (e => { e.preventDefault(); const originalEvent = e.originalEvent, delta = originalEvent.deltaY; if (originalEvent.ctrlKey || originalEvent.metaKey) { const isZoomIn = delta < 0, offset = this.$container.offset(), mouseX = originalEvent.pageX - offset.left, mouseY = originalEvent.pageY - offset.top; this.handleZoom(isZoomIn, mouseX, mouseY); } else { this.state.currentPos.top -= delta * this.settings.scrollSpeed; if (!this.state.ticking) { this.state.ticking = !0; requestAnimationFrame((() => { this.updateImgStyle(); this.state.ticking = !1; })); } } })); this.$layero.on("click", ".viewer-tools i, .thumb-item", (e => { e.stopPropagation(); const $target = $(e.currentTarget), action = $target.data("action"), index = $target.data("index"); if (void 0 !== index) return this.switchImage(index); this.toolBarAction(action); })); this.$container.on("click", (e => { e.target === this.$container[0] && layer2.close(this.layerIdx); })); } toolBarAction(action) { switch (action) { case "zoomIn": this.handleZoom(!0); break; case "zoomOut": this.handleZoom(!1); break; case "rotate": this.state.rotate += 90; this.updateImgStyle(); break; case "reset": this.state.scale = 1; this.state.rotate = 0; this.state.currentPos = this._calculatePosition("auto"); this.updateImgStyle(); } } init() { this.state.scale = this._getInitialScale(this.urls[this.state.currentIndex]); this.layerIdx = layer2.open({ type: layer2.typeMap.VIEWER, title: !1, area: [ "100%", "100%" ], shade: [ .9, "#000" ], scrollbar: !1, skin: "layui-layer-photos", content: this.render(), anim: !1, isOutAnim: !1, success: layero => { this.$layero = layero; this.$img = layero.find("#viewer-img"); this.$container = layero.find(".viewer-main"); this.bindEvents(); this.onImageReady((() => { if (this.settings.toTop) { this.state.currentPos = this._calculatePosition("top"); this.updateImgStyle(); } })); }, end: () => { $(document).off(".PhotoViewer"); } }); } } layer2.showImageViewer = (urls, config) => { var _a; try { const parent = unsafeWindow.parent; if (parent !== unsafeWindow && (null == (_a = parent.layer) ? void 0 : _a.showImageViewer)) return parent.layer.showImageViewer(urls, config); } catch (e) {} return new PhotoViewer(urls, config); }; }(); 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")); } static xmlTo$dom(xmlString) { const parser = new DOMParser, xmlDoc = parser.parseFromString(xmlString, "application/xml"), parseError = xmlDoc.getElementsByTagName("parsererror"); if (parseError.length > 0) { console.error("XML Parse Error:", parseError[0].textContent); return $(parser.parseFromString(xmlString, "text/html")); } return $(xmlDoc); } }; __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 = { info: "linear-gradient(to right, #60A5FA, #93C5FD)", success: "linear-gradient(to right, #10B981, #6EE7B7)", error: "linear-gradient(to right, #EF4444, #FCA5A5)" }, coreShowMessage = (msg, type, finalOptions) => { (() => { const id = "toast-style"; if (document.getElementById(id)) return; const showCss = `\n <style id="${id}">\n .toast-container {\n position: fixed;\n z-index: 9999999999;\n display: flex;\n flex-direction: column;\n gap: 10px;\n pointer-events: none;\n transition: all 0.3s ease;\n }\n .toast-item {\n pointer-events: auto;\n min-width: 180px;\n max-width: 400px;\n padding: 12px 20px;\n border-radius: 12px;\n color: white;\n font-family: sans-serif;\n box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n opacity: 0;\n transform: translateY(20px) scale(0.9);\n transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n word-break: break-all;\n position: relative;\n text-align: center;\n box-sizing: border-box;\n }\n .toast-item.show {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n .toast-close {\n margin-left: 10px;\n cursor: pointer;\n float: right;\n font-size: 18px;\n line-height: 1;\n opacity: 0.7;\n }\n .toast-close:hover { opacity: 1; }\n </style>\n `; document.head.insertAdjacentHTML("beforeend", showCss); })(); const [x, y] = finalOptions.offset || [ "right", "bottom" ], time = finalOptions.time ?? ("error" === type ? 2500 : 2e3), container = ((x, y) => { const id = `toast-container-${x}-${y}`; let container = document.getElementById(id); if (!container) { container = document.createElement("div"); container.id = id; container.className = "toast-container"; if ("left" === x) container.style.left = "20px"; else if ("right" === x) container.style.right = "20px"; else if ("center" === x) { container.style.left = "50%"; container.style.transform = "center" === y ? "translate(-50%, -50%)" : "translateX(-50%)"; } if ("top" === y) container.style.top = "20px"; else if ("bottom" === y) container.style.bottom = "20px"; else if ("center" === y) { container.style.top = "50%"; container.style.transform = "translateY(-50%)"; } document.body.appendChild(container); } return container; })(x, y), toast = document.createElement("div"); toast.className = "toast-item"; toast.style.background = colors[type]; let html = `<span>${msg}</span>`; (-1 === time || finalOptions.close) && (html = '<span class="toast-close">×</span>' + html); toast.innerHTML = html; const removeToast = () => { toast.classList.remove("show"); setTimeout((() => { toast.parentNode && toast.parentNode.removeChild(toast); const cb = finalOptions.callback || finalOptions.onClose; "function" == typeof cb && cb(); }), 400); }; let hideTimer = null; const startTimer = () => { -1 !== time && (hideTimer = setTimeout(removeToast, time)); }, stopTimer = () => { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } }; if (-1 !== time) { toast.onmouseenter = stopTimer; toast.onmouseleave = startTimer; } const closeBtn = toast.querySelector(".toast-close"); closeBtn && (closeBtn.onclick = e => { e.stopPropagation(); removeToast(); }); "top" === y ? container.prepend(toast) : container.appendChild(toast); requestAnimationFrame((() => toast.classList.add("show"))); startTimer(); return { toastElement: toast, closeShow: removeToast }; }, 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 (e) {} return String(part); })).join(" "); msg.length > 500 && (msg = msg.substring(0, 500) + "... (内容超长已省略)"); return coreShowMessage(msg, type, configOverrides); }; return { ok: (...msg) => runShow("success", ...msg), error: (...msg) => runShow("error", ...msg), info: (...msg) => runShow("info", ...msg), config: options => createToastInterface({ ...configOverrides, ...options }) }; }, baseShow = createToastInterface({}); "undefined" != typeof unsafeWindow && (unsafeWindow.show = baseShow); 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 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 (e) { console.error(`插件 ${name2} 加载 CSS 失败`, e); return { name: name2, status: "rejected", error: e }; } })))).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 (e) { console.error(`插件 ${name2} 执行失败`, e); } }))); } } 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 - 130, event.clientX - 150 ]); 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; } static getResponsiveArea(defaultArea = [ "85%", "90%" ]) { return window.innerWidth >= 1200 ? defaultArea : [ "90%", "90%" ]; } }; _intervalContainer = new WeakMap; __privateAdd(_CommonUtil, _intervalContainer, {}); let CommonUtil = _CommonUtil; class MagnetHubPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "fileEmoji", "📄"); __publicField(this, "folderEmoji", "📁"); __publicField(this, "maxHistoryCount", 20); __publicField(this, "currentSort", ""); __publicField(this, "currentSearchTicket", 0); __publicField(this, "highlightKeywords", [ "-c", "破解", "流出", "-AI", "无码", "4k", "8k", "-uc", "-u" ]); __publicField(this, "engineConfig", [ { engineId: "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) }, { engineId: "btdig", label: "BTDIG", targetPage: "https://btdig.com/search?q={keyword}", url: "https://btdig.com/search?q={keyword}", handler: (url, keyword) => this.parseBtdig(url, keyword) }, { engineId: "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), parseDetailPage: async detailUrl => { const html = await gmHttp.get(detailUrl), $nodes = DomUtil.htmlTo$dom(html).find("#torrent-description p"); let fileListHtml = "", fileCount = 0; for (let i = 0; i < $nodes.length; i++) { const text = $($nodes[i]).text().trim(); if (text) { fileListHtml += `<div class="file-item" title="${text}">${this.fileEmoji} ${text} </div>`; fileCount++; } } return { fileListHtml: fileListHtml, fileCount: fileCount }; } }, { engineId: "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), parseDetailPage: async detailUrl => { const html = await gmHttp.get(detailUrl), $liNodes = DomUtil.htmlTo$dom(html).find(".torrent-file-list.panel-body").find("li"); let fileListHtml = "", fileCount = 0; for (let i = 0; i < $liNodes.length; i++) { const $li = $($liNodes[i]), $folderLink = $li.children("a.folder"), paddingLeft = 12 * ($li.parents("ul").length - 1); if ($folderLink.length > 0) { const folderName = $folderLink.text().trim(); fileListHtml += `<div class="file-item file-item-folder" style="padding-left: ${paddingLeft}px;">${this.folderEmoji} ${folderName}</div>`; } else { let text = $li.contents().filter((function() { return 3 === this.nodeType; })).text().trim(); text || (text = $li.text().trim()); if (!text) continue; fileListHtml += `<div class="file-item" style="padding-left: ${paddingLeft}px;" title="${text}">${this.fileEmoji} ${text}</div>`; fileCount++; } } return { fileListHtml: fileListHtml, fileCount: fileCount }; } }, { engineId: "btsow", label: "BTSOW", targetPage: "https://btsow.lol/search/{keyword}", url: "https://btsow.lol/bts/data/api/search", handler: (url, keyword) => this.parseBTSOW(url, keyword), parseDetailPage: async detailUrl => { const hashMatch = detailUrl.match(/detail\/([A-F0-9]{40})/i), hashId = hashMatch ? hashMatch[1] : ""; if (!hashId) throw new Error("无法从URL解析Hash ID"); const res = await gmHttp.postJson("https://btsow.lol/bts/data/api/magnet", [ hashId ]); if (!res || 200 !== res.code || !res.data) throw new Error(`API 请求失败: ${res ? res.code : "无响应"}`); const files = res.data.files || []; if (0 === files.length) throw new Error("该资源暂无文件明细"); let fileListHtml = ""; files.forEach((file => { fileListHtml += `<div class="file-item">${this.fileEmoji} ${file.filename}</div>`; })); return { fileListHtml: fileListHtml, fileCount: files.length }; } } ]); } 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 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 .file-item {\n font-size: 12px; \n color: #475569; \n overflow: hidden; \n text-overflow: ellipsis; \n white-space: nowrap;\n padding: 2px 0;\n }\n .file-item-folder {\n font-weight: bold; \n color: #1e293b;\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: layer.typeMap.HTML, title: "磁力搜索", content: '<div id="magnetHubBox"></div>', area: CommonUtil.getResponsiveArea([ "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 savedEngines = cacheManager.getItem(cacheManager.magnetHubEngines_key, [ this.engineConfig[0].engineId ]), tabsHtml = this.engineConfig.map((engine => `\n <label class="engine-checkbox-wrapper" data-engine-id="${engine.engineId}">\n <input type="checkbox" class="engine-checkbox" value="${engine.engineId}" ${savedEngines.includes(engine.engineId) ? "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 `); $hubContainer.append($container); this.bindMagnetEvents($container); } bindMagnetEvents($container) { const $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", (e => { const $target = $(e.currentTarget); $searchInput.val($target.data("val")); performSearch(); })); $container.on("click", ".history-del-btn", (e => { e.stopPropagation(); const valToRemove = $(e.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", (e => { 13 === e.which && performSearch(); })); $container.on("click", ".sort-item", (e => { const $target = $(e.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(); })); $container.off("click.copy").on("click.copy", ".magnet-copy-btn", (function(e) { e.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 e => { e.preventDefault(); const magnet = $(e.currentTarget).data("magnet"), cacheKey = "whatslink_" + magnet, cacheData = tempCacheManager.getItem(cacheKey, []); if (CommonUtil.isNotNull(cacheData)) { layer.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); layer.showImageViewer(imgList, { toTop: !1, initZoom: !1 }); } catch (err) { console.error("预览请求失败:", err); show.error("获取预览图失败:", err); } finally { loadObj.close(); } })); $container.on("contextmenu", ".engine-label-text", (e => { let targetUrl = $(e.currentTarget).attr("data-url"); if (!targetUrl) return; e.preventDefault(); const currentKw = $searchInput.val().trim(); targetUrl = currentKw ? targetUrl.replace("{keyword}", encodeURIComponent(currentKw)) : targetUrl.replace("{keyword}", ""); window.open(targetUrl, "_blank"); })); renderHistory(); performSearch(); } 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 engineId => { const engine = this.engineConfig.find((e => e.engineId === engineId)), $engineWrapper = $container.find(`.engine-checkbox-wrapper[data-engine-id="${engineId}"]`); $engineWrapper.addClass("engine-loading"); try { let isFromFetch = !1; const cacheKey = `${engine.engineId}_${keyword}`; this.cacheKey = cacheKey; let results = tempCacheManager.getItem(cacheKey, []); if (CommonUtil.isNull(results)) { const url = engine.url.replace("{keyword}", encodeURIComponent(keyword)); results = await engine.handler(url, keyword); isFromFetch = !0; } if (ticket !== this.currentSearchTicket) return; results.forEach((item => { item.source = engine.label; item.engineId = engine.engineId; const hash = this._getMagnetHash(item.magnet), existingIndex = allResults.findIndex((existing => this._getMagnetHash(existing.magnet) === hash)); if (-1 === existingIndex) 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; (currentHasFileList && !existingHasFileList || currentFileCount > existingFileCount) && (allResults[existingIndex] = item); } })); isFromFetch && CommonUtil.isNotNull(results) && tempCacheManager.setItem(cacheKey, results); $engineWrapper.removeClass("engine-loading").addClass("engine-success"); this.displayResults($resultsContainer, allResults, duplicateCount); } catch (e) { console.error(`${engine.label} 搜索失败:`, e); const errorMessage = e.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((item => { let canFetch = !1, engineCfg = null; if (!item.fileListHtml) { engineCfg = this.engineConfig.find((e => e.engineId === item.engineId)); engineCfg && "function" == typeof engineCfg.parseDetailPage && (canFetch = !0); } let highlightedTitle = item.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="${item.detailPageUrl}" target="_blank" title="${item.title}">\n ${highlightedTitle}\n </a>\n <div class="magnet-meta">\n <span style="color: #007bff; font-weight: bold;">[ ${item.source} ]</span>\n <span style="margin: 0 4px;">|</span>\n <span>📦 ${item.size || "未知"}</span>\n <span style="margin: 0 4px;">|</span>\n <span>📅 ${item.date || "未知"}</span>\n <span class="file-count-wrapper">\n ${item.fileCount ? `\n <span style="margin: 0 4px;">|</span>\n <span class="file-count-label">${this.fileEmoji} 文件数 ${item.fileCount}</span>\n ` : ""}\n </span>\n </div>\n </div>\n \n <div class="magnet-actions">\n <jhs-btn type="aliceBlue" class="magnet-preview-btn" data-magnet="${item.magnet}">预览</jhs-btn>\n <jhs-btn type="white" class="magnet-copy-btn" data-magnet="${item.magnet}">复制</jhs-btn>\n <jhs-btn type="royalBlue" class="magnet-btn-download"><a href="${item.magnet}">立即下载</a></jhs-btn>\n <jhs-btn type="denimBlue" class="magnet-115-btn magnet-down-115" data-magnet="${item.magnet}">115离线</jhs-btn>\n </div>\n </div>\n\n <div class="file-list-area" style="margin-top: 10px;">\n ${item.fileListHtml ? `<div class="file-list-container">${item.fileListHtml}</div>` : canFetch ? '<div class="fetch-placeholder"></div>' : ""}\n </div>\n </div>\n `); canFetch && this.renderLoadBtn($result.find(".fetch-placeholder"), item, engineCfg); $container.append($result); })); } renderLoadBtn($placeholder, item, engineCfg, isRetry = !1) { const $btnWrapper = $(`\n <div class="fetch-action-container" style="padding: 12px; border: 1px dashed ${isRetry ? "#f87171" : "#e2e8f0"}; border-radius: 8px; background: ${isRetry ? "#fef2f2" : "#f8fafc"}; transition: all 0.3s;">\n <jhs-btn type="white" class="load-files-btn" style="font-size: 12px; cursor: pointer; display: inline-flex; align-items: center; gap: 4px;">\n ${isRetry ? "<span>❌ 加载失败,点击重试</span>" : "<span>🔍 查看文件列表</span>"}\n </jhs-btn>\n </div>\n `); $btnWrapper.find(".load-files-btn").on("click", (async e => { $(e.currentTarget).prop("disabled", !0).html("<span>⏳ 正在努力加载中...</span>"); $btnWrapper.css("border-color", "#cbd5e1"); try { const detailData = await engineCfg.parseDetailPage(item.detailPageUrl); if (detailData.fileListHtml) { Object.assign(item, detailData); let cachedResults = tempCacheManager.getItem(this.cacheKey, []); if (cachedResults && cachedResults.length > 0) { const targetIdx = cachedResults.findIndex((r => r.magnet === item.magnet)); if (-1 !== targetIdx) { Object.assign(cachedResults[targetIdx], detailData); tempCacheManager.setItem(this.cacheKey, cachedResults); } } $placeholder.html(`<div class="file-list-container" style="animation: fadeIn 0.3s;">${item.fileListHtml}</div>`); } else $placeholder.html('<div style="font-size: 12px; color: #94a3b8; text-align: center; padding: 10px;">暂无文件列表</div>'); if (detailData.fileCount) { const $wrapper = $placeholder.closest(".magnet-result").find(".file-count-wrapper"), $label = $wrapper.find(".file-count-label"); $label.length > 0 ? $label.html(`${this.fileEmoji} 文件数 ${item.fileCount}`) : $wrapper.html(`<span style="margin: 0 4px;">|</span> <span class="file-count-label">${this.fileEmoji} 文件数 ${item.fileCount}</span> `); $wrapper.find(".file-count-label").css("color", "#10b981").fadeOut(400).fadeIn(800, (function() { setTimeout((() => { $(this).css({ color: "", transition: "color 0.5s" }); }), 1e3); })); } } catch (err) { console.error(`[${engineCfg.label}] 详情解析异常:`, err); this.renderLoadBtn($placeholder, item, engineCfg, !0); } })); $placeholder.empty().append($btnWrapper); } 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}`, title = item.name.replace(/<[^>]*>?/gm, ""); title.toLowerCase().includes(keyword.toLowerCase()) && results.push({ detailPageUrl: detailPageUrl, title: title, 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, ""), $fileNodes = $el.find(".torrent_excerpt div[class*='fa-']"), fileItems = []; let minPadding = 999; $fileNodes.each(((_, div) => { const $div = $(div), text = $div.text().trim(); if (!text) return; const paddingMatch = ($div.attr("style") || "").match(/padding-left:\s*([\d.]+)em/), emValue = paddingMatch ? parseFloat(paddingMatch[1]) : 0; emValue < minPadding && (minPadding = emValue); const isFolder = $div.hasClass("fa-folder-open") || $div.hasClass("fa-folder"), $nextSpan = $div.next("span"), sizeText = $nextSpan.length ? $nextSpan.text().trim() : "", fullText = sizeText ? `${text} (${sizeText})` : text; fileItems.push({ text: fullText, isFolder: isFolder, em: emValue }); })); const fileListHtml = fileItems.map((item => { const finalPadding = Math.max(0, 12 * (item.em - minPadding)); return `<div class="file-item ${item.isFolder ? "file-item-folder" : ""}" style="padding-left: ${finalPadding}px;" title="${item.text}">\n ${item.isFolder ? this.folderEmoji : this.fileEmoji} ${item.text}\n </div>`; })).join(""); magnet && results.push({ detailPageUrl: detailPageUrl, title: title, magnet: magnet, size: size, date: dateChinese, fileCount: fileCount, fileListHtml: fileListHtml || '<div class="file-item">暂无文件列表</div>', sizeByte: this._quickParseSize ? this._quickParseSize(size) : 0 }); })); 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">${this.fileEmoji} ${name2}</div>`)).join("") : '<div class="file-item">暂无文件列表</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 (e) {} 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 => { if (!(event instanceof KeyboardEvent)) return; 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", blacklist: [] }); __publicField(this, "mousePos", { x: 0, y: 0 }); __publicField(this, "$menu", null); __publicField(this, "$currentTarget", null); __publicField(this, "menuConfig", [ { id: "imageSearch", label: "🖼️ 以图识图", visible: ctx => ctx.isImage, handler: async () => { var _a; const src = null == (_a = this.$currentTarget) ? void 0 : _a.attr("src"); this.getBean("ImageRecognitionPlugin").openRecognition(src); } }, { id: "translation", label: "📖 翻译助手", visible: ctx => ctx.hasText, handler: async event => { const selectedText = window.getSelection().toString().trim(), pos = { x: (null == event ? void 0 : event.clientX) || 0, y: (null == event ? void 0 : event.clientY) || 0 }, translationPlugin = this.getBean("TranslationPlugin"); translationPlugin && await translationPlugin.openTranslation(selectedText, pos); } }, { id: "magnetSearch", label: "🧲 磁力聚合", visible: ctx => ctx.hasText, handler: async () => { const selectedText = window.getSelection().toString().trim(); this.getBean("MagnetHubPlugin").openMagnetHubDialog(selectedText); } }, { id: "magnetExtractor", label: "🔍 提取本页磁力", visible: ctx => !ctx.hasText && !ctx.isImage, handler: async () => { this.getBean("MagnetExtractorPlugin").startExtractor(); } } ]); } getName() { return "MenuPlugin"; } getRegisterCondition() { return !0; } async handle() { await this.loadSettings(); this.registerGMMenu(); this.refreshHotkey(); this.render(); this.bindEvents(); } async loadSettings() { const saved = cacheManager.getItem(cacheManager.menuSetting_key); saved && (this.settings = { ...this.settings, ...saved }); } refreshHotkey() { this.hotkeyId && HotkeyManager.unregisterHotkey(this.hotkeyId); this.hotkeyId = HotkeyManager.registerHotkey(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(); } })); } registerGMMenu() { var _a; this.gmMenuIds && this.gmMenuIds.forEach((id => GM_unregisterMenuCommand(id))); this.gmMenuIds = []; const host = window.location.host, toggleLabel = (this.settings.blacklist || []).includes(host) ? "❌ 已禁用 (点击启用)" : "✅ 已启用 (点击禁用)"; this.gmMenuIds.push(GM_registerMenuCommand(toggleLabel, (() => { this.toggleSite(host); }))); this.gmMenuIds.push(GM_registerMenuCommand(`管理禁用列表 (${(null == (_a = this.settings.blacklist) ? void 0 : _a.length) || 0})`, (() => { this.openBlacklistManager(); }))); this.gmMenuIds.push(GM_registerMenuCommand(`设置触发快捷键 (${this.settings.triggerHotkey})`, (() => { this.openHotkeySetter(); }))); } toggleSite(host) { if (this.settings.blacklist.includes(host)) { this.settings.blacklist = this.settings.blacklist.filter((h => h !== host)); show.ok("已启用,刷新页面后生效"); } else { this.settings.blacklist.push(host); show.error("已禁用,刷新页面后生效"); } cacheManager.setItem(cacheManager.menuSetting_key, this.settings); window.location.reload(); } openBlacklistManager() { const list = this.settings.blacklist || [], content = `\n <div class="blacklist-container" style="padding:15px; max-height:300px; overflow-y:auto;">\n ${list.length > 0 ? list.map((site => `\n <div class="blacklist-item" style="display:flex; justify-content:space-between; align-items:center; padding:10px; border-bottom:1px solid #eee;">\n <span style="font-size:14px; color:#333;">${site}</span>\n <button class="remove-site-btn" data-site="${site}" style="color:#ff4d4f; border:1px solid #ff4d4f; background:none; padding:2px 8px; border-radius:4px; cursor:pointer;">移除</button>\n </div>\n `)).join("") : '<div style="text-align:center; padding:20px; color:#999;">黑名单为空</div>'}\n </div>\n `; layer.open({ type: layer.typeMap.HTML, title: "📋 禁用网站管理", area: [ "350px", "400px" ], content: content, btn: [ "关闭" ], success: (layero, index) => { layero.find(".remove-site-btn").on("click", (e => { const siteToRemove = $(e.currentTarget).data("site"); this.settings.blacklist = this.settings.blacklist.filter((s => s !== siteToRemove)); cacheManager.setItem(cacheManager.menuSetting_key, this.settings); $(e.currentTarget).closest(".blacklist-item").fadeOut(300, (function() { $(this).remove(); 0 === layero.find(".blacklist-item").length && layero.find(".blacklist-container").html('<div style="text-align:center; padding:20px; color:#999;">黑名单为空</div>'); })); this.registerGMMenu(); show.ok(`已移除 ${siteToRemove}`); })); } }); } 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: layer.typeMap.HTML, title: "⚙️ 设置触发快捷键", area: [ "400px", "250px" ], content: content, btn: [ "确定", "取消" ], shadeClose: !0, scrollbar: !1, anim: -1, 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" : ""}" data-id="${item.id || ""}"> <span>${item.label}</span> </li>`); 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", (e => { if (!ticking) { window.requestAnimationFrame((() => { this.mousePos.x = e.clientX; this.mousePos.y = e.clientY; ticking = !1; })); ticking = !0; } })); const debouncedEntry = DomUtil.debounce((e => { const $target = $(e.target); $target.closest(".plugin-alt-menu").length || (this.$currentTarget = $target); }), 100); $(document).on("mouseenter", "*", (e => { debouncedEntry(e); })); $(document).on("mouseleave", "img", (e => { const nextElement = e.relatedTarget || e.originalEvent.relatedTarget; this.$menu && (this.$menu.is(nextElement) || this.$menu.has(nextElement).length > 0) || (this.$currentTarget = null); })); document.addEventListener("mousedown", (e => { $(e.target).closest(".plugin-alt-menu").length || this.hideMenu(); })); this.$menu.off("click").on("click", "li", (e => { e.stopPropagation(); const handler = $(e.currentTarget).data("menu-handler"); if (handler && "function" == typeof handler) { handler(e); this.hideMenu(); } })); window.addEventListener("scroll", (() => { this.hideMenu(); }), { capture: !0, passive: !0 }); } showMenu2() { if (!this.$menu) return; const selectedText = window.getSelection().toString().trim(), isImage = this.$currentTarget && this.$currentTarget.is("img"); this.$menu.find("li").hide(); isImage ? this.$menu.find('[data-id="imageSearch"]').show() : selectedText ? this.$menu.find('[data-id="magnetSearch"]').show() : this.$menu.find("li").show(); this.renderPosition(); } showMenu() { if (!this.$menu) return; const context = { hasText: "" !== window.getSelection().toString().trim(), isImage: this.$currentTarget && this.$currentTarget.is("img") }, isEmptyState = !context.hasText && !context.isImage; this.menuConfig.forEach((item => { const $li = this.$menu.find(`[data-id="${item.id}"]`), isVisible = !!isEmptyState || (!item.visible || item.visible(context)); $li.toggle(isVisible); })); this.renderPosition(); } renderPosition() { this.$menu.css({ visibility: "hidden", display: "block" }); const menuWidth = this.$menu.outerWidth(), menuHeight = this.$menu.outerHeight(), winWidth = window.innerWidth, winHeight = window.innerHeight, x = this.mousePos.x, y = this.mousePos.y; let finalX = x + menuWidth > winWidth ? winWidth - menuWidth - 10 : x, finalY = y + menuHeight > winHeight ? y - menuHeight - 10 : y; this.$menu.css({ top: Math.max(0, finalY), left: Math.max(0, finalX), visibility: "visible" }).show(); } hideMenu() { this.$menu && this.$menu.is(":visible") && 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, "no")); } 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 cursor: pointer;\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: layer.typeMap.HTML, 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", (e => { e.preventDefault(); $(e.currentTarget).addClass("highlight"); })); $layero.on("dragleave drop", "#upload-area", (e => { e.preventDefault(); $(e.currentTarget).removeClass("highlight"); if ("drop" === e.type) { const files = e.originalEvent.dataTransfer.files; files && files[0] && this.handleImageFile(files[0]); } })); $layero.on("click", (e => { const $target = $(e.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) { e.stopPropagation(); CommonUtil.q(e, "确认清空所有搜索历史吗?", (() => { cacheManager.setItem(cacheManager.image_recognition_history_key, []); this.renderHistory(); })); } else "retry-btn" === id && this.searchByImage().then(); })); $layero.on("change", "#image-file", (e => { e.target.files && e.target.files[0] && this.handleImageFile(e.target.files[0]); })); $(document).off("paste.searchImg").on("paste.searchImg", (async e => { const items = e.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", (e => { this.autoOpenEnabled = $(e.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 = e => { $previewImage.attr("src", e.target.result); $previewImage.attr("data-original", e.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(e) { e.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", (e => { e.stopPropagation(); this.deleteHistoryItem(e, item); })); $list.append($item); })); } else $container.hide(); } deleteHistoryItem(e, item) { CommonUtil.q(e, "确认删除这条搜索记录吗?", (() => { 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(); })); } } 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 (e) { console.error("Error:", articleUrl, e); } } })); } 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 cloud115Api_getSign = async () => { const res = await gmHttp.get("https://115.com/?ct=offline&ac=space&_=" + (new Date).getTime()); return "object" == typeof res ? res : null; }, cloud115Api_getDownPathList = async () => await gmHttp.get("https://webapi.115.com/offine/downpath"), cloud115Api_saveDownPath = async dirId => { const formData = new FormData; formData.append("file_id", dirId); return await gmHttp.postFormData("https://webapi.115.com/offine/downpath", formData); }, cloud115Api_addTaskUrl = async (magnet, downPathId, uid, sign, time) => { const formData = new FormData; formData.append("url", magnet); formData.append("wp_path_id", downPathId); 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); }, cloud115Api_addTaskUrls = async (magnets, downPathId, uid, sign, time) => { const formData = new FormData; formData.append("wp_path_id", downPathId); 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); }, cloud115Api_getTaskLists = 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; }, cloud115Api_addDir = 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); }; 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 (e) { show.error("发生错误:" + e); console.error(e); } 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 cloud115Api_getSign(); if (!singInfo) { show.error("未登录115网盘", { close: !0, duration: -1, callback: () => window.open("https://115.com") }); return; } const sign = singInfo.sign, time = singInfo.time, {downPathId: downPathId, userId: userId} = await this.getDownPathInfo(); let result; const isBatch = magnets.length > 1; result = isBatch ? await cloud115Api_addTaskUrls(magnets, downPathId, userId, sign, time) : await cloud115Api_addTaskUrl(magnets[0], downPathId, userId, sign, time); console.log(isBatch ? "[115] 批量离线返回值:" : "[115] 单条离线返回值:", result); let title; title = !1 === result.state ? result.error_msg + " 是否前往查看?" : isBatch ? `成功添加 ${magnets.length} 个任务,是否前往查看?` : "添加成功, 是否前往查看?"; CommonUtil.q(null, title, (async () => { let targetUrl = "https://115.com/?tab=offline&mode=wangpan"; if (!isBatch) { const fileId = await this.getFileId(userId, sign, time, result.info_hash); console.log("[115] 获取文件id:", fileId); fileId && (targetUrl = `https://115.com/?cid=${fileId}&offset=0&mode=wangpan`); } window.open(targetUrl); })); } async getDownPathInfo() { let res = await cloud115Api_getDownPathList(), downPathList = res.data; const mapInfo = item => ({ downPathId: item.file_id, userId: item.user_id }); if (downPathList && downPathList.length > 0) return mapInfo(downPathList[0]); show.info("没有默认离线目录, 正在创建中..."); const dirId = (await cloud115Api_addDir("云下载")).file_id; await cloud115Api_saveDownPath(dirId); show.info("创建完成, 开始执行离线下载"); res = await cloud115Api_getDownPathList(); downPathList = res.data; if (downPathList && downPathList.length > 0) return mapInfo(downPathList[0]); throw new Error("获取115离线目录信息失败:" + res.error); } async getFileId(userId, sign, time, infoHash) { const taskList = await cloud115Api_getTaskLists(userId, sign, time); console.log("[115] 离线任务列表", taskList); 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: 0, right: 0 }); __publicField(this, "panelConfig", { expandedWidth: 350, collapsedWidth: 220 }); } getName() { return "MagnetExtractorPlugin"; } getRegisterCondition() { return !0; } async initCss() { const prefix = "#magnet-extractor-side"; return `\n <style>\n /* 基础容器:增加 ID 权重并强制重置 */\n ${prefix} {\n all: initial; /* 清除继承样式 */\n position: fixed !important;\n width: ${this.panelConfig.expandedWidth}px !important;\n max-height: 85vh !important;\n background: #ffffff !important;\n box-shadow: 0 8px 32px rgba(0,0,0,0.2) !important;\n border-radius: 12px !important;\n display: flex !important;\n flex-direction: column !important;\n z-index: 1989101 !important; /* 置于顶层 */\n border: 1px solid #eeeeee !important;\n font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif !important;\n overscroll-behavior: contain !important;\n user-select: none !important;\n box-sizing: border-box !important;\n transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;\n }\n \n ${prefix} * {\n box-sizing: border-box !important;\n font-family: inherit !important;\n }\n \n /* 折叠状态 */\n ${prefix}.collapsed {\n max-height: 48px !important;\n width: ${this.panelConfig.collapsedWidth}px !important;\n overflow: hidden !important;\n }\n \n /* 头部区域 */\n ${prefix} .magnet-header {\n padding: 12px 15px !important;\n background: #f8f9fa !important;\n border-bottom: 1px solid #eeeeee !important;\n display: flex !important;\n justify-content: space-between !important;\n align-items: center !important;\n border-radius: 12px 12px 0 0 !important;\n cursor: move !important;\n height: 48px !important;\n }\n \n ${prefix} .magnet-header h3 {\n margin: 0 !important;\n padding: 0 !important;\n font-size: 14px !important;\n font-weight: bold !important;\n color: #333333 !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n background: transparent !important;\n }\n \n /* 控件组 */\n ${prefix} .magnet-controls {\n display: flex !important;\n gap: 8px !important;\n align-items: center !important;\n background: transparent !important;\n }\n \n /* 列表容器 */\n ${prefix} .magnet-list-container {\n flex: 1 !important;\n overflow-y: auto !important;\n padding: 10px !important;\n background: #ffffff !important;\n }\n \n /* 列表项 */\n ${prefix} .magnet-item {\n padding: 8px 12px !important;\n border-bottom: 1px solid #f5f5f5 !important;\n display: flex !important;\n align-items: center !important;\n justify-content: space-between !important;\n background: #ffffff !important;\n gap: 5px;\n }\n \n ${prefix} .magnet-link-text {\n font-size: 12px !important;\n color: #0066cc !important;\n \n display: inline-block !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n \n vertical-align: middle !important;\n flex-grow: 1;\n cursor: pointer;\n }\n \n /* 按钮组容器 */\n ${prefix} .magnet-item-footer {\n display: flex !important;\n gap: 4px !important;\n flex-shrink: 0 !important;\n }\n \n /* 按钮通用样式重置 */\n ${prefix} .m-btn, \n ${prefix} .copy-all-btn, \n ${prefix} .down-all-115-btn {\n appearance: none !important;\n border: 1px solid #ddd !important;\n background: #fff !important;\n border-radius: 4px !important;\n cursor: pointer !important;\n font-size: 12px !important;\n line-height: 1 !important;\n padding: 6px 10px !important;\n text-align: center !important;\n transition: all 0.2s !important;\n outline: none !important;\n }\n \n ${prefix} .m-btn-copy { color: #4CAF50 !important; border-color: #4CAF50 !important; }\n ${prefix} .m-btn-copy:hover { background: #4CAF50 !important; color: #fff !important; }\n \n ${prefix} .magnet-down-115 { \n background-color: #2562ff !important; \n color: #ffffff !important; \n border: none !important; \n font-weight: bold !important;\n }\n ${prefix} .magnet-down-115:hover { background-color: #1a4cd8 !important; }\n \n ${prefix} .m-btn-preview { \n color: #ff9800 !important; \n border-color: #ff9800 !important; \n }\n ${prefix} .m-btn-preview:hover { \n background: #ff9800 !important; \n color: #fff !important; \n }\n \n /* 页脚 */\n ${prefix} .magnet-panel-footer {\n padding: 12px !important;\n border-top: 1px solid #eeeeee !important;\n background: #ffffff !important;\n display: flex !important;\n flex-direction: column !important;\n gap: 8px !important;\n }\n \n ${prefix} .copy-all-btn {\n background: #4CAF50 !important;\n color: white !important;\n padding: 10px !important;\n font-weight: bold !important;\n }\n \n /* 图标样式 */\n ${prefix} .magnet-ctrl-icon {\n width: 28px !important;\n height: 28px !important;\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n border-radius: 6px !important;\n color: #666 !important;\n background: transparent !important;\n }\n \n ${prefix} .magnet-ctrl-icon svg {\n width: 18px !important;\n height: 18px !important;\n stroke: currentColor !important;\n fill: none !important;\n }\n \n /* 动画 */\n @keyframes magnet-focus-elegant {\n 0% { \n box-shadow: 0 0 0 0px rgba(33, 150, 243, 0.8);\n background-color: rgba(33, 150, 243, 0.1);\n }\n 50% { \n box-shadow: 0 0 0 10px rgba(33, 150, 243, 0);\n background-color: rgba(33, 150, 243, 0.3);\n }\n 100% { \n box-shadow: 0 0 0 0px rgba(33, 150, 243, 0);\n background-color: transparent;\n }\n }\n\n .magnet-target-highlight {\n /* 蓝色底色 + 粗下划线 */\n border-bottom: 3px solid #2196f3 !important;\n border-radius: 2px;\n animation: magnet-focus-elegant 1s cubic-bezier(0.4, 0, 0.2, 1) forwards !important;\n transition: all 0.2s ease;\n }\n </style>\n `; } async handle() { const cachedStatus = cacheManager.getItem(cacheManager.magnetExtractorCollapsed_key); this.isCollapsed = !0 === cachedStatus; const cachedPos = cacheManager.getItem("magnet_extractor_pos_key"); cachedPos && (this.position = { top: cachedPos.top, right: cachedPos.right }); this.startExtractor(!0); } startExtractor(isSilent = !1) { this.extractLinks(); 0 !== this.links.length ? this.renderPanel() : isSilent || show.info("本页未发现磁力或 ed2k 链接"); } extractLinks() { const regexes = [ /magnet:\?xt=urn:btih:[a-zA-Z0-9]{32,40}/gi, /ed2k:\/\/\|file\|[^|]+\|\d+\|[a-fA-F0-9]{32}\|/gi ], foundMap = new Map, collectMatches = (str, element) => { if (str) for (const reg of regexes) { const matches = str.match(reg); if (matches) for (const m of matches) { const url = m.replace(/[\r\n]/g, "").trim(); foundMap.has(url) || foundMap.set(url, element); } } }; $("a[href]").each(((i, el) => collectMatches($(el).attr("href"), el))); const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { acceptNode: node2 => { const parentTag = node2.parentElement.tagName; return [ "A", "SCRIPT", "STYLE" ].includes(parentTag) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; } }); let node; for (;node = walker.nextNode(); ) collectMatches(node.nodeValue, node); this.links = Array.from(foundMap.entries()).map((([url, element]) => ({ url: url, element: element }))); } 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; right: ${this.position.right}px;">\n <div class="magnet-header">\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 m-btn-locate" title="点击定位所在位置" data-index="${index}">${item.url}</span>\n <div class="magnet-item-footer">\n <button class="m-btn m-btn-preview" data-magnet="${item.url}">预览</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", (e => { e.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", (e => { e.stopPropagation(); $panel.remove(); })); $panel.on("click", ".m-btn-locate", (e => { const idx = $(e.currentTarget).data("index"); let target = this.links[idx].element; if (target) { if (3 === target.nodeType) { const span = document.createElement("span"); target.before(span); span.appendChild(target); this.links[idx].element = span; target = span; } const $target = $(target), offset = $target.offset().top - $(window).height() / 2; $("html, body").stop().animate({ scrollTop: offset }, 500, (() => { $target.addClass("magnet-target-highlight"); setTimeout((() => $target.removeClass("magnet-target-highlight")), 1e3); })); } })); $panel.on("click", ".m-btn-copy", (e => { const $btn = $(e.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", (e => { const $btn = $(e.currentTarget); this.copyToClipboard(this.links.map((m => m.url)).join("\n")); const originalText = $btn.text(); $btn.text("全部复制成功! ✅"); setTimeout((() => $btn.text(originalText)), 1500); })); $panel.on("click", ".m-btn-preview", (async e => { e.preventDefault(); const magnet = $(e.currentTarget).data("magnet"), cacheKey = "whatslink_" + magnet, cacheData = tempCacheManager.getItem(cacheKey, []); if (cacheData && cacheData.length > 0) { layer.showImageViewer(cacheData, { toTop: !1, initZoom: !1 }); return; } const loadObj = loading(); try { const url = `https://whatslink.info/api/v1/link?url=${encodeURIComponent(magnet)}`, res = await gmHttp.get(url); if (!res || !Array.isArray(res.screenshots) || 0 === res.screenshots.length) { show.error("该磁力链接暂无预览图"); return; } const imgList = res.screenshots.filter((item => item && item.screenshot)).map((item => item.screenshot)); if (0 === imgList.length) { show.error("未发现有效的预览图片地址"); return; } tempCacheManager.setItem(cacheKey, imgList); layer.showImageViewer(imgList, { toTop: !1, initZoom: !1 }); } catch (err) { console.error("预览请求失败:", err); show.error("获取预览图失败:", err); } finally { loadObj.close(); } })); } initDragEvents() { const $panel = $("#magnet-extractor-side"), $header = $panel.find(".magnet-header"); let isDragging = !1, offset = { x: 0, y: 0 }; $header.on("mousedown", (e => { if ($(e.target).closest(".magnet-controls").length) return; isDragging = !0; const rect = $panel[0].getBoundingClientRect(); offset = { x: rect.right - e.clientX, y: e.clientY - rect.top }; $panel.css("transition", "none"); $(document).on("mousemove.magnet_drag", (de => { if (!isDragging) return; const winW = window.innerWidth, winH = window.innerHeight, panelW = $panel.outerWidth(), panelH = $panel.outerHeight(); let newRight = winW - de.clientX - offset.x, newTop = de.clientY - offset.y; newRight = Math.max(0, Math.min(newRight, winW - panelW)); newTop = Math.max(0, Math.min(newTop, winH - panelH)); $panel.css({ right: newRight + "px", top: newTop + "px", left: "auto" }); this.position = { top: newTop, right: newRight }; })); $(document).on("mouseup.magnet_drag", (() => { if (isDragging) { isDragging = !1; $(document).off(".magnet_drag"); $panel.css("transition", "width 0.3s cubic-bezier(0.4, 0, 0.2, 1)"); 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); } } class Hjd2048Plugin extends BasePlugin { getName() { return "Hjd2048Plugin"; } getRegisterCondition() { return $("title").text().includes("人人为我论坛"); } async handle() { this.hookJs(); } hookJs() { document.getElementsByClassName = function(className, oBox) { this.d = oBox && document.getElementById(oBox) || document; for (var children = this.d.getElementsByTagName("*") || document.all, elements = [], ii = 0; ii < children.length; ii++) for (var child = children[ii], classNames = ("string" == typeof child.className ? child.className : child.getAttribute("class") || "").split(" "), j = 0; j < classNames.length; j++) if (classNames[j] === className) { elements.push(child); break; } return elements; }; } } class TranslationPlugin extends BasePlugin { getName() { return "TranslationPlugin"; } getRegisterCondition() { return !0; } async initCss() { return '\n <style>\n #trans-panel {\n width: 420px;\n background: #ffffff; \n border-radius: 0 0 12px 12px; /* 底部圆角 */\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\n box-sizing: border-box;\n }\n .trans-body { padding: 20px; }\n \n .trans-header-row {\n display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;\n }\n .trans-selector-group { display: flex; align-items: center; gap: 8px; }\n .trans-select {\n font-size: 13px; border: 1px solid #dcdfe6; border-radius: 6px; padding: 4px 8px; \n outline: none; cursor: pointer; background: #fff; color: #606266;\n }\n .trans-hint { font-size: 11px; color: #bbb; }\n \n .trans-input {\n width: 100%; height: 110px; border: 1px solid #dcdfe6; border-radius: 8px; \n padding: 12px; margin-bottom: 15px; resize: none; box-sizing: border-box; \n font-size: 14px; line-height: 1.6; outline: none; display: block;\n transition: border-color 0.2s;\n }\n .trans-input:focus { border-color: #409eff; }\n \n .trans-submit-btn {\n background: #409eff; color: white; border: none; padding: 8px 20px; \n border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500;\n }\n .trans-submit-btn:hover { opacity: 0.9; }\n \n .trans-result {\n background: #f9fafb; padding: 15px; border-radius: 8px; \n min-height: 80px; color: #333; font-size: 14px; line-height: 1.7;\n word-break: break-word; max-height: 250px; overflow-y: auto; \n border: 1px solid #eee; margin-top: 15px;\n }\n \n .trans-result-container {\n position: relative;\n margin-top: 15px;\n }\n .trans-copy-btn {\n background: #fff;\n border: 1px solid #dcdfe6;\n color: #606266;\n padding: 8px 20px; \n border-radius: 6px; \n cursor: pointer; \n font-size: 14px; \n font-weight: 500;\n display: none;\n }\n .trans-copy-btn:hover {\n color: #409eff;\n border-color: #c6e2ff;\n background-color: #ecf5ff;\n }\n </style>\n '; } detectDirection(text) { return /^[\u4e00-\u9fa5]{4,}/.test(text) ? { src: "zh-CN", target: "en" } : { src: "en", target: "zh-CN" }; } async openTranslation(text = "") { const $existingLayer = $(`#layui-layer${this.layerIndex}`); null === this.layerIndex || 0 === $existingLayer.length ? await this.createLayer(text) : this.updateLayerState($existingLayer, text); } async createLayer(text = "") { const direction = this.detectDirection(text); this.layerIndex = layer.open({ type: 1, title: "翻译助手", area: "420px", shade: 0, shadeClose: !1, content: `\n <div id="trans-panel">\n <div class="trans-body">\n <div class="trans-header-row">\n <div class="trans-selector-group">\n <select class="trans-src-lang trans-select">\n <option value="en" ${"en" === direction.src ? "selected" : ""}>英语</option>\n <option value="zh-CN" ${"zh-CN" === direction.src ? "selected" : ""}>中文</option>\n <option value="ja">日语</option>\n </select>\n <span style="color: #ccc;">➔</span>\n <select class="trans-target-lang trans-select">\n <option value="zh-CN" ${"zh-CN" === direction.target ? "selected" : ""}>中文</option>\n <option value="en" ${"en" === direction.target ? "selected" : ""}>英语</option>\n <option value="ja">日语</option>\n </select>\n </div>\n <span class="trans-hint">↵ 翻译 / ⇧↵ 换行</span>\n </div>\n <textarea class="trans-input" placeholder="输入内容...">${text}</textarea>\n <div style="display: flex; justify-content: flex-end;">\n <button class="trans-copy-btn" style="margin-right: 5px">复制结果</button>\n <button class="trans-submit-btn">立即翻译</button>\n </div>\n <div class="trans-result-container">\n <div class="trans-result">${text ? "正在翻译..." : "等待输入内容..."}</div>\n </div>\n </div>\n </div>\n `, success: layero => { this.bindEvents(layero); text && layero.find(".trans-submit-btn").click(); } }); } updateLayerState($layer, text) { const direction = this.detectDirection(text), $input = $layer.find(".trans-input"); $layer.find(".trans-src-lang").val(direction.src); $layer.find(".trans-target-lang").val(direction.target); $input.val(text).focus(); $layer.find(".trans-submit-btn").click(); } bindEvents(layero) { const $input = layero.find(".trans-input"), $result = layero.find(".trans-result"), $src = layero.find(".trans-src-lang"), $target = layero.find(".trans-target-lang"), $copyBtn = layero.find(".trans-copy-btn"), refresh = async () => { const val = $input.val().trim(); if (!val) return; const $btn = layero.find(".trans-submit-btn"); $btn.prop("disabled", !0).css({ opacity: "0.6", cursor: "not-allowed" }).text("翻译中..."); $result.text("正在翻译..."); $copyBtn.hide(); try { const res = await (async (text, sourceLang = "ja", targetLang = "zh-CN") => { if (!text) throw new Error("翻译文本不能为空"); const url = "https://translate-pa.googleapis.com/v1/translate?" + new URLSearchParams({ "params.client": "gtx", dataTypes: "TRANSLATION", key: "AIzaSyDLEeFI5OtFBwYBIoK_jj5m32rZK5CkCXA", "query.sourceLanguage": sourceLang, "query.targetLanguage": targetLang, "query.text": text }), res = await fetch(url); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); return (await res.json()).translation; })(val, $src.val(), $target.val()); $result.text(res); res && $copyBtn.show(); } catch (e) { $result.text("翻译失败: " + e.message); } finally { $btn.prop("disabled", !1).css({ opacity: "", cursor: "" }).text("立即翻译"); } }; layero.find(".trans-submit-btn").on("click", refresh); $src.on("change", refresh); $target.on("change", refresh); $input.on("keydown", (e => { if (13 === e.keyCode && !e.shiftKey) { e.preventDefault(); refresh().then(); } })).focus(); $copyBtn.on("click", (() => { const textToCopy = $result.text(); textToCopy && CommonUtil.copyToClipboard(textToCopy, (() => { const originalText = $copyBtn.text(); $copyBtn.text("已复制!").css("color", "#67c23a"); setTimeout((() => { $copyBtn.text(originalText).css("color", ""); }), 1500); }), (() => { show.error("复制失败"); })); })); } } !async function() { const savedSettings = cacheManager.getItem(cacheManager.menuSetting_key), blacklist = (null == savedSettings ? void 0 : savedSettings.blacklist) || [], host = window.location.host; if (blacklist.includes(host)) { const menuPlugin = new MenuPlugin; await menuPlugin.loadSettings(); menuPlugin.registerGMMenu(); return; } const pluginManager = new PluginManager; pluginManager.register(MenuPlugin); pluginManager.register(ImageRecognitionPlugin); pluginManager.register(WangPan115TaskPlugin); pluginManager.register(MagnetHubPlugin); pluginManager.register(MagnetExtractorPlugin); pluginManager.register(TranslationPlugin); pluginManager.register(SeHuaTangPlugin); pluginManager.register(Hjd2048Plugin); pluginManager.processCss().then(); pluginManager.processPlugins().then(); }(); }();